Security audit #32
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Security Audit Report — Mycelium Portal
Scope: Full-stack security review of
mycportal— Rust/Axum backend, Dioxus/WASM frontend, deployment configs, CI/CD pipelinesAuditor: Opus 4.6 (Claude Code)
Executive Summary
Mycelium Portal is a full-stack blockchain interaction portal (TFChain + Hero Ledger/NEAR) with a Rust/Axum REST backend, a Dioxus/WASM single-page frontend, and Docker/Kubernetes deployment. The application handles real financial transactions (TFT token transfers, SPORE bridging).
Overall posture: Moderate risk. The architecture is sound — client-side signing, no database, Rust memory safety — but several exploitable issues exist in the API layer, financial calculations, and deployment configuration that must be addressed before production use.
Critical Findings
C1. CORS Allows All Origins
File:
crates/backend/src/main.rs:54-57Impact: Any website on the internet can make API requests to the backend. An attacker can host a malicious page that calls
/api/transfer/prepareand/api/transfer/submitusing a victim's session state. Since the prepare/submit flow relies only on session IDs (not origin-bound tokens), a cross-origin attacker can initiate transactions.Recommendation: Restrict
allow_originto the actual deployment domains:C2. No Signer Identity Verification on Transfer Submit
File:
crates/backend/src/api.rs:373-472The
transfer_submitendpoint accepts anysigner_accountand never verifies it matches thefromaccount stored in thePendingTransfercreated duringtransfer_prepare.The same issue exists in
opt_out_v3_submitat line 562 —req.signer_accountis not compared topending.signer_account.Impact: If an attacker obtains a valid session ID (e.g., via CORS abuse from C1, or network sniffing), they can submit a transaction with a different signer account.
Recommendation:
C3. Float-to-Integer Precision Loss in Financial Calculations
File:
crates/backend/src/api.rs:311Impact:
f64cannot precisely represent all decimal values. For example:0.1 * 10_000_000.0may yield999999.9999...which truncates to999999instead of10000001.1 * 10_000_000.0=10999999.999...→10999999(loses 1 planck)In a financial application, this means users can lose funds due to rounding. Over many transactions, the cumulative loss could be significant.
Recommendation: Parse the amount as a string, split on the decimal point, and compute planck units with integer arithmetic:
C4. Missing Security Headers in Caddy Reverse Proxy
File:
deploy/CaddyfileThe entire Caddyfile is:
No security headers are set.
Impact:
Recommendation:
C5. Container Runs as Root
File:
Dockerfile:22-39The runtime stage has no
USERdirective. The application runs asroot(UID 0).Impact: If the application is compromised, the attacker gains root privileges inside the container, with potential to escape the container namespace, modify mounted volumes, or install persistence mechanisms.
Recommendation:
C6. SSH MITM Vulnerability in Deployment Pipeline
File:
.forgejo/workflows/deploy-staging.yml:74Impact:
StrictHostKeyChecking=accept-newaccepts any host key on first connection. An attacker performing a man-in-the-middle attack on the CI runner's network gets root shell access to the staging server. This is compounded by deploying asroot.Recommendation: Pre-populate
known_hostswith the server's fingerprint in the workflow, and deploy as a dedicated non-root user with minimal sudo privileges.High Findings
H1. No Rate Limiting on Any Endpoint
Files:
crates/backend/src/main.rs,crates/backend/src/api.rsAll 22+ API endpoints are completely unprotected against abuse.
Impact:
/api/transfer/preparecan be spammed to fill theDashMapwith millions of pending sessions (memory exhaustion DoS)/api/balance/{account_id}can be used to enumerate all on-chain accountsRecommendation: Add
tower::limit::RateLimitLayeror a per-IP rate limiter. At minimum, rate-limit the prepare/submit endpoints.H2. Session ID Memory Exhaustion (DoS)
File:
crates/backend/src/api.rs:352-363andcrates/backend/src/state.rs:90-94Each call to
/api/transfer/prepareinserts into an unboundedDashMap. Cleanup runs every 30 seconds and retains entries for 5 minutes. An attacker sending 100k requests/second for 30 seconds creates 3 million entries before cleanup fires.Impact: Out-of-memory crash, denial of service.
Recommendation: Add a maximum size to the pending maps. Reject new sessions when the limit is reached. Add per-IP rate limiting.
H3. Mnemonic Stored as Plaintext in WASM Memory
File:
crates/frontend/src/signing.rs:15-19WASM linear memory is inspectable via browser DevTools (
WebAssembly.Memory). The mnemonic — which controls all funds — persists for the entire session.Recommendation: Use the
zeroizecrate to clear the mnemonic after deriving the keypair, or at minimum document this as a known risk. Consider not storing the mnemonic inWalletStateat all after initial derivation.H4. No NaN/Infinity Validation on Transfer Amount
File:
crates/backend/src/api.rs:311-317If
amount_tftisNaNorInfinity(both valid JSON floats), theas u128cast produces0for NaN (passes the check as 0, but blocked) and a large value for Infinity.f64::INFINITY as u128is platform-dependent.Impact: Undefined behavior in financial calculations.
Recommendation: Add
if !req.amount_tft.is_finite() || req.amount_tft <= 0.0 { return Err(...) }H5. Unpinned Git Dependency (Supply Chain Risk)
File:
crates/backend/Cargo.toml:8No
rev,tag, orbranchspecified. A compromised upstream repo could inject malicious code that runs in your backend with full access to the RPC client and pending transactions.Recommendation: Pin to a specific commit:
rev = "abc1234...". Also pin theheroledger_gateway_clientdependency in the frontend.H6. Missing Kubernetes Security Context
File:
deploy/k8s/deployment.ymlThe deployment lacks all security context specifications:
runAsNonRoot: truereadOnlyRootFilesystem: trueallowPrivilegeEscalation: falsecapabilities: drop: [ALL]NetworkPolicy(lateral movement possible within cluster)Recommendation: Add comprehensive
securityContextto both Pod and Container specs.H7. Unpinned Docker Base Images
Files:
Dockerfile:1,22,deploy/docker-compose.yml:3,deploy/k8s/deployment.yml:19FROM rust:latest— non-deterministic buildFROM debian:bookworm-slim— no digest pinimage: .../www-migrate-mycelium:latest— both Docker Compose and KubernetesImpact: Supply chain attack vector. A compromised upstream image silently enters production. Builds are not reproducible.
Recommendation: Pin all images to specific digests:
rust:1.77-bookworm@sha256:...Medium Findings
M1. Error Messages Leak Internal Details
Files:
crates/backend/src/api.rs(multiple locations)Error messages like
format!("Chain error: {e}")andformat!("Failed to create partial tx: {e}")expose internal chain RPC errors, Substrate metadata details, and infrastructure information to API consumers.Recommendation: Return generic error messages to clients. Log detailed errors server-side only.
M2. No CSRF Protection
File:
crates/frontend/src/api.rsAll API calls use simple POST/GET with only a
Content-Type: application/jsonheader. No CSRF tokens, no custom headers for same-origin validation. Combined with the open CORS policy (C1), this makes cross-site request forgery trivial.Recommendation: After fixing CORS (C1), add a custom header requirement (e.g.,
X-Requested-With) that simple CORS requests cannot send.M3. Explorer
farm_idsPath Parameter Not ValidatedFile:
crates/backend/src/api.rs:691-700The
farm_idsstring is passed directly to the Explorer API without validation. Depending on how the Explorer constructs its HTTP query, this could enable parameter injection (e.g., extra query parameters or path traversal).Recommendation: Parse
farm_idsas a comma-separated list of integers and reject invalid input.M4. No Content Security Policy from Application
File:
crates/frontend/src/main.rs:147-170The frontend loads external resources from CDNs (Bootstrap CSS, Bootstrap Icons, Google Fonts) without Subresource Integrity (SRI) hashes. A compromised CDN could inject malicious CSS.
Recommendation: Add SRI hashes to all external resource links and enforce CSP from the server (see C4).
M5. CI/CD Input Injection Risk
File:
.forgejo/workflows/build-container.yml:62-76User-provided input from
github.event.inputs.versionis used directly in Docker tags without sanitization:Recommendation: Validate version input with a regex:
^[a-zA-Z0-9._-]+$M6. Hardcoded Test Password in E2E Tests
File:
e2e/tests/vault-flow.spec.ts:7Recommendation: Use an environment variable:
process.env.VAULT_TEST_PASSWORD || "testpassword123"M7. Single Token for Multiple CI/CD Purposes
File:
.forgejo/workflows/build-container.ymlFORGEJO_TOKENis used for git cloning, Docker registry authentication, AND release API calls. Compromising one system compromises all three.Recommendation: Create separate tokens with minimal required permissions for each purpose.
Low Findings
L1. No Request Timeout on Backend
File:
crates/backend/src/main.rs:103axum::servehas no timeout configuration. A slow client can hold connections open indefinitely, potentially exhausting file descriptors.Recommendation: Add
tower::timeout::TimeoutLayer.L2. Health Endpoint Exposes Internal State
File:
crates/backend/src/api.rs:17-32/api/healthreturns the latest block number, network name, and connection status without authentication — useful reconnaissance data.L3. Integer Truncation in Explorer Responses
File:
crates/backend/src/api.rs:672-674If the Explorer returns values exceeding
u32::MAX, these silently truncate.L4. No Vault Brute-Force Protection
File:
crates/frontend/src/components/login.rs:269-300The vault unlock flow has no rate limiting or lockout after failed password attempts. An attacker with access to the browser can brute-force the vault password offline since the encrypted data is in
localStorage.Recommendation: Use a slow KDF (Argon2id) with high cost parameters for vault encryption. Document that vault security depends on password strength.
L5. Logout Does Not Clear Sensitive State
File:
crates/frontend/src/components/login.rs:161-165Setting signals to
Nonedoes not zeroize the previous value from WASM memory. The mnemonic and keypair may remain in memory after logout until garbage collected or overwritten.What's Done Well
subxt-signerlibrary.gitignoreproperly excludes.env, no secrets committedPriority Action Items
Before Publishing (P0)
req.signer_accounttopending.fromis_finite()check (H4)Before Production (P1)
towerrate limit middlewarerev = "..."to Cargo.tomlImprovement (P2)
farm_idsparameter (M3)zeroizecrate, clear after derivationMethodology
This audit was conducted through static analysis of all source code, configuration files, CI/CD pipelines, and deployment manifests. The following files were reviewed in full:
crates/backend/src/main.rs(104 lines)crates/backend/src/api.rs(822 lines)crates/backend/src/state.rs(95 lines)crates/shared/src/lib.rs(255 lines)crates/frontend/src/main.rs(1102 lines)crates/frontend/src/signing.rs(172 lines)crates/frontend/src/api.rs(117 lines)crates/frontend/src/heroledger.rs(296 lines)crates/frontend/src/config.rs(8 lines)crates/frontend/src/components/login.rs(606 lines)crates/frontend/src/components/transfer.rs(635 lines)crates/frontend/src/components/balance.rs(84 lines)crates/frontend/src/components/sidebar.rs(208 lines)crates/frontend/src/pages/prepare_pre_register.rs(393 lines)Dockerfiledeploy/Caddyfile,deploy/docker-compose.ymldeploy/k8s/deployment.yml,deploy/k8s/ingress.yml,deploy/k8s/service.yml,deploy/k8s/configmap.yml.forgejo/workflows/test.yml,build-container.yml,deploy-staging.ymle2e/tests/vault-flow.spec.tsCargo.tomlfiles and.gitignoreNo dynamic testing, penetration testing, or dependency vulnerability scanning (e.g.,
cargo audit) was performed. These are recommended as follow-up activities.Excellent points.