Renewed security audit #41

Open
opened 2026-03-02 08:47:16 +00:00 by emre · 0 comments
Owner

This audit is done by Opus 4.6 to verify fixes to the previous security audit mycelium/www_migrate_mycelium#32.

Stack: Rust backend (Axum) + Rust/WASM frontend (Dioxus) + Caddy reverse proxy
Architecture: Frontend signs transactions in WASM, backend prepares/submits to TFChain


CRITICAL Issues

1. CSP blocks external fonts and CSS (breaks production UI)

File: deploy/Caddyfile:7

The Content-Security-Policy allows style-src 'self' 'unsafe-inline' and default-src 'self', but the frontend loads Bootstrap CSS, Bootstrap Icons, and Google Fonts from CDNs:

  • cdn.jsdelivr.net (Bootstrap CSS + Icons)
  • fonts.googleapis.com / fonts.gstatic.com

These will be blocked in production by the CSP. Either the CSP must be updated, or these assets must be self-hosted.

Fix: Update the CSP in the Caddyfile to include the CDN domains, or bundle assets locally (preferred for security).

2. No rate limiting on API endpoints

File: crates/backend/src/main.rs:70-93

There is no rate limiting middleware on any API endpoint. The MAX_PENDING cap (10,000) on prepare sessions prevents unbounded memory growth, but:

  • An attacker can still spam 10,000 prepare requests, consuming server resources
  • Read-only endpoints (/balance, /twin, /farm, /node) have zero rate limiting, enabling DoS against the TFChain WebSocket connection
  • Each request creates a new chain query, so a flood can exhaust the WebSocket connection

Fix: Add tower_governor or similar rate-limiting middleware, especially on /transfer/prepare, /node/opt-out-v3/prepare, and all chain-query endpoints.

3. Mnemonic held in memory for entire session

Files: crates/frontend/src/signing.rs:18, crates/frontend/src/main.rs:98

WalletState stores the mnemonic phrase in a plain String for the entire browser session lifetime. In WASM, this stays in linear memory and cannot be securely zeroed. If an attacker can read WASM memory (e.g., via a browser extension or Spectre-class attack), the mnemonic is exposed.

Recommendation: This is a known limitation of WASM. Consider using the vault-only flow by default (where the mnemonic is decrypted, used briefly for key derivation, then discarded) and documenting the risk.


HIGH Issues

4. No font-src in CSP — web fonts will be blocked

File: deploy/Caddyfile:7

The CSP has no font-src directive. Default falls to default-src 'self', blocking Google Fonts woff2 files from fonts.gstatic.com and Bootstrap Icons font files from cdn.jsdelivr.net.

5. connect-src only allows devnet gateway in production

File: deploy/Caddyfile:7

The CSP connect-src includes https://ledger.dev.projectmycelium.com:9090 (devnet) but the config also references a production gateway. Meanwhile, the default gateway URL compiled into the frontend is the devnet gateway:

File: crates/frontend/src/config.rs:6

option_env!("HEROLEDGER_GATEWAY_URL").unwrap_or("https://ledger.dev.projectmycelium.com:9090")

If HEROLEDGER_GATEWAY_URL is not set at build time, production will use the devnet gateway. This is a configuration risk that could cause production to interact with dev infrastructure.

6. Docker container runs as root

File: Dockerfile:22-38

The runtime stage doesn't create or switch to a non-root user. The binary runs as root inside the container, which increases the blast radius of any container escape.

Fix: Add RUN useradd -r portal and USER portal before the CMD.

7. Session ID is not tied to any client identity

File: crates/backend/src/api.rs:373-384

The transfer_prepare endpoint returns a session_id (UUIDv4), and transfer_submit only checks that the signer_account matches the original from. However, the session_id is the only secret protecting the pending transfer. If an attacker guesses or intercepts the session_id, they can submit a different (malicious) signature, although the chain would reject an invalid signature.

The real risk: an attacker who intercepts the session_id can trigger the submit to consume the session, causing a denial-of-service on the legitimate user's transfer attempt.

Recommendation: Consider adding a client-side nonce or binding the session to additional client identity (e.g., a hash of the signing payload).


MEDIUM Issues

8. MAX_PENDING is very high (10,000)

File: crates/backend/src/state.rs:10

10,000 pending sessions × (the size of PendingTransfer struct) could use a non-trivial amount of memory. An attacker could create 10,000 sessions in rapid succession before cleanup runs.

Fix: Lower to 1,000 or add per-IP rate limiting.

9. No Permissions-Policy header

File: deploy/Caddyfile

Modern browsers support Permissions-Policy (formerly Feature-Policy) to restrict access to browser features like camera, microphone, geolocation, etc. This header is missing.

Fix: Add Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" to the Caddy headers.

10. Floating-point arithmetic for financial amounts

Files: crates/backend/src/api.rs:327, crates/shared/src/lib.rs:78

amount_tft is an f64, and conversion to planck uses (req.amount_tft * 10_000_000.0).round() as u128. While the .round() mitigates most precision issues, using floating-point for financial calculations is inherently risky. For example, 0.1 * 10_000_000.0 = 999999.9999999999 before rounding.

The tests cover basic cases, and .round() handles them. This is low-risk but worth noting for future amounts near the precision boundary.

11. No secure attribute on vault localStorage data

File: crates/frontend/src/components/login.rs:53-74

The vault-to-TFChain address mapping is stored in localStorage with key vault_tfchain_addresses. While the actual encrypted vault uses the Web Crypto API (which is good), the address mapping is stored in plaintext. An XSS attack could read this mapping to identify which accounts are saved.

This is low-impact since only public addresses are stored, not private keys.

12. CDN dependency for CSS/fonts (supply chain risk)

File: crates/frontend/src/main.rs:157-168

Bootstrap CSS/Icons and Google Fonts are loaded from CDNs without Subresource Integrity (SRI) hashes. A CDN compromise could inject malicious CSS/JS.

Fix: Either self-host these assets or add integrity attributes.

Summary of Required Actions (Priority Order)

Priority Issue Action
CRITICAL CSP blocks CDN assets Update CSP or self-host Bootstrap/fonts
HIGH Docker runs as root Add non-root user to Dockerfile
HIGH Default gateway is devnet Ensure HEROLEDGER_GATEWAY_URL is set in production builds
HIGH No API rate limiting Add tower_governor middleware
MEDIUM Missing Permissions-Policy Add header in Caddyfile
MEDIUM No SRI on CDN resources Add integrity hashes or self-host
MEDIUM MAX_PENDING too high Lower to 1,000 + per-IP limiting
LOW Float for financial amounts Document precision guarantees
This audit is done by Opus 4.6 to verify fixes to the previous security audit mycelium/www_migrate_mycelium#32. **Stack:** Rust backend (Axum) + Rust/WASM frontend (Dioxus) + Caddy reverse proxy **Architecture:** Frontend signs transactions in WASM, backend prepares/submits to TFChain --- ## CRITICAL Issues ### 1. CSP blocks external fonts and CSS (breaks production UI) **File:** `deploy/Caddyfile:7` The Content-Security-Policy allows `style-src 'self' 'unsafe-inline'` and `default-src 'self'`, but the frontend loads Bootstrap CSS, Bootstrap Icons, and Google Fonts from CDNs: - `cdn.jsdelivr.net` (Bootstrap CSS + Icons) - `fonts.googleapis.com` / `fonts.gstatic.com` These will be **blocked** in production by the CSP. Either the CSP must be updated, or these assets must be self-hosted. **Fix:** Update the CSP in the Caddyfile to include the CDN domains, or bundle assets locally (preferred for security). ### 2. No rate limiting on API endpoints **File:** `crates/backend/src/main.rs:70-93` There is no rate limiting middleware on any API endpoint. The `MAX_PENDING` cap (10,000) on prepare sessions prevents unbounded memory growth, but: - An attacker can still spam 10,000 prepare requests, consuming server resources - Read-only endpoints (`/balance`, `/twin`, `/farm`, `/node`) have zero rate limiting, enabling DoS against the TFChain WebSocket connection - Each request creates a new chain query, so a flood can exhaust the WebSocket connection **Fix:** Add `tower_governor` or similar rate-limiting middleware, especially on `/transfer/prepare`, `/node/opt-out-v3/prepare`, and all chain-query endpoints. ### 3. Mnemonic held in memory for entire session **Files:** `crates/frontend/src/signing.rs:18`, `crates/frontend/src/main.rs:98` `WalletState` stores the mnemonic phrase in a plain `String` for the entire browser session lifetime. In WASM, this stays in linear memory and cannot be securely zeroed. If an attacker can read WASM memory (e.g., via a browser extension or Spectre-class attack), the mnemonic is exposed. **Recommendation:** This is a known limitation of WASM. Consider using the vault-only flow by default (where the mnemonic is decrypted, used briefly for key derivation, then discarded) and documenting the risk. --- ## HIGH Issues ### 4. No `font-src` in CSP — web fonts will be blocked **File:** `deploy/Caddyfile:7` The CSP has no `font-src` directive. Default falls to `default-src 'self'`, blocking Google Fonts woff2 files from `fonts.gstatic.com` and Bootstrap Icons font files from `cdn.jsdelivr.net`. ### 5. `connect-src` only allows devnet gateway in production **File:** `deploy/Caddyfile:7` The CSP `connect-src` includes `https://ledger.dev.projectmycelium.com:9090` (devnet) but the config also references a production gateway. Meanwhile, the default gateway URL compiled into the frontend is the **devnet** gateway: **File:** `crates/frontend/src/config.rs:6` ```rust option_env!("HEROLEDGER_GATEWAY_URL").unwrap_or("https://ledger.dev.projectmycelium.com:9090") ``` If `HEROLEDGER_GATEWAY_URL` is not set at build time, production will use the **devnet** gateway. This is a configuration risk that could cause production to interact with dev infrastructure. ### 6. Docker container runs as root **File:** `Dockerfile:22-38` The runtime stage doesn't create or switch to a non-root user. The binary runs as root inside the container, which increases the blast radius of any container escape. **Fix:** Add `RUN useradd -r portal` and `USER portal` before the `CMD`. ### 7. Session ID is not tied to any client identity **File:** `crates/backend/src/api.rs:373-384` The `transfer_prepare` endpoint returns a `session_id` (UUIDv4), and `transfer_submit` only checks that the `signer_account` matches the original `from`. However, the session_id is the **only** secret protecting the pending transfer. If an attacker guesses or intercepts the session_id, they can submit a different (malicious) signature, although the chain would reject an invalid signature. The real risk: an attacker who intercepts the session_id can trigger the submit to **consume** the session, causing a denial-of-service on the legitimate user's transfer attempt. **Recommendation:** Consider adding a client-side nonce or binding the session to additional client identity (e.g., a hash of the signing payload). --- ## MEDIUM Issues ### 8. `MAX_PENDING` is very high (10,000) **File:** `crates/backend/src/state.rs:10` 10,000 pending sessions × (the size of `PendingTransfer` struct) could use a non-trivial amount of memory. An attacker could create 10,000 sessions in rapid succession before cleanup runs. **Fix:** Lower to 1,000 or add per-IP rate limiting. ### 9. No `Permissions-Policy` header **File:** `deploy/Caddyfile` Modern browsers support `Permissions-Policy` (formerly `Feature-Policy`) to restrict access to browser features like camera, microphone, geolocation, etc. This header is missing. **Fix:** Add `Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"` to the Caddy headers. ### 10. Floating-point arithmetic for financial amounts **Files:** `crates/backend/src/api.rs:327`, `crates/shared/src/lib.rs:78` `amount_tft` is an `f64`, and conversion to planck uses `(req.amount_tft * 10_000_000.0).round() as u128`. While the `.round()` mitigates most precision issues, using floating-point for financial calculations is inherently risky. For example, `0.1 * 10_000_000.0 = 999999.9999999999` before rounding. The tests cover basic cases, and `.round()` handles them. This is low-risk but worth noting for future amounts near the precision boundary. ### 11. No `secure` attribute on vault localStorage data **File:** `crates/frontend/src/components/login.rs:53-74` The vault-to-TFChain address mapping is stored in `localStorage` with key `vault_tfchain_addresses`. While the actual encrypted vault uses the Web Crypto API (which is good), the address mapping is stored in plaintext. An XSS attack could read this mapping to identify which accounts are saved. This is low-impact since only public addresses are stored, not private keys. ### 12. CDN dependency for CSS/fonts (supply chain risk) **File:** `crates/frontend/src/main.rs:157-168` Bootstrap CSS/Icons and Google Fonts are loaded from CDNs without Subresource Integrity (SRI) hashes. A CDN compromise could inject malicious CSS/JS. **Fix:** Either self-host these assets or add `integrity` attributes. --- ## Summary of Required Actions (Priority Order) | Priority | Issue | Action | |----------|-------|--------| | **CRITICAL** | CSP blocks CDN assets | Update CSP or self-host Bootstrap/fonts | | **HIGH** | Docker runs as root | Add non-root user to Dockerfile | | **HIGH** | Default gateway is devnet | Ensure `HEROLEDGER_GATEWAY_URL` is set in production builds | | **HIGH** | No API rate limiting | Add `tower_governor` middleware | | **MEDIUM** | Missing `Permissions-Policy` | Add header in Caddyfile | | **MEDIUM** | No SRI on CDN resources | Add integrity hashes or self-host | | **MEDIUM** | MAX_PENDING too high | Lower to 1,000 + per-IP limiting | | **LOW** | Float for financial amounts | Document precision guarantees |
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mycelium/www_migrate_mycelium#41
No description provided.