Files
bal-server/docs/08_security_audit.md
svatantrya b8d87ecabd fix: per-endpoint rate limits, request timeout and input hardening
bal-server:
- Wire independent actix-governor buckets per endpoint class (pushtxs,
  searchtx, info/stats, default) so polling a cheap endpoint can no longer
  exhaust the push-txs budget; previously every limit was dead config
- Enforce BAL_SERVER_ACTIX_TIMEOUT_SECS via a request-timeout middleware that
  returns 408 Request Timeout, covering slow request bodies
- De-duplicate txids within a single /pushtxs request body
- Replace the byte-slice xpub detection (panic on non-ASCII) with a safe
  prefix check

bal-pusher:
- Handle malformed/empty ZMQ frames without indexing panics or UTF-8 expect
- Return an error from sign_message and skip the stats report instead of
  panicking when the private key cannot be parsed

validation / db:
- Reject IPv4-mapped IPv6 targets (::ffff:a.b.c.d) in is_valid_welist_url
  to close an SSRF bypass, with unit tests
- Use Path::starts_with for the SQLite forbidden-directory check so paths
  like /etcfoo are not misclassified

misc:
- Add explicit secret patterns to .gitignore and sync the docs
2026-10-14 18:30:23 -04:00

160 lines
11 KiB
Markdown

# Security Audit
## Quick Reference
- **What this file contains:** threat model, vulnerability assessment, hardening recommendations, and a security checklist.
- **See also:** [AGENTS.md](AGENTS.md), [07_deployment_and_ops.md](07_deployment_and_ops.md), [06_database_schema.md](06_database_schema.md), [03_architecture_and_data_flow.md](03_architecture_and_data_flow.md), [04_modules_detail.md](04_modules_detail.md)
---
## Threat Model
### Assets
1. **`bal.db` (SQLite database):** Contains all transaction details, user IP addresses, and stats data. Single unencrypted file on disk. WAL mode enabled for concurrent access safety.
2. **Private Keys (`privkey.pem`):** Used to sign statistics payloads for the `welist` server. Located in `.gitignore`.
3. **Bitcoin Node (`bitcoind`) Access:** The `bal-pusher` has RPC access. Compromise allows arbitrary transaction broadcasting.
4. **Server Availability (`bal-server`):** Public-facing HTTP endpoint. DoS attacks threaten service availability.
### Attackers
- **Remote Anonymous Users:** Can interact with the API via public HTTP. No credentials required.
- **Network Man-in-the-Middle (MITM):** TLS termination is via Nginx reverse proxy. The `bal-server` itself is plain HTTP.
- **Local/Insider Threats:** If the server is compromised, the attacker can access `bal.db`, private keys, and env files.
---
## Vulnerability Assessment
### 1. SQL Injection (FIXED)
**Severity:** HIGH | **Status:** Fixed
**Location:** `src/db.rs`, `src/bin/bal-server.rs`, `src/bin/bal-pusher.rs`
**Description:** SQL queries previously used `format!()` for string interpolation. All queries now use parameterized statements (`?` with `bind()`).
**Mitigation Applied:** All SQL queries rewritten with prepared statements. `execute_insert` uses parameterized batch inserts. `check_duplicate_txids` uses parameterized `IN` clauses. `echo_stats` handler uses prepared statements for chain filtering.
**Regression Tests:** `tests/sql_injection_tests.rs` (3 tests).
### 2. Panic on Untrusted Input (FIXED)
**Severity:** HIGH | **Status:** Fixed
**Location:** `src/bin/bal-server.rs`, `src/bin/bal-pusher.rs`
**Description:** All `unwrap()`/`expect()` calls on critical paths have been replaced with safe error handling.
**Mitigation Applied:**
- `bal-server`: `from_utf8` returns 400, `sqlite::open` uses `open_db()` with validation, all handlers return proper HTTP status codes.
- `bal-pusher`: RPC failures log errors + sleep + retry (no panic), ZMQ `recv` uses `set_rcvtimeo(5000)`, malformed ZMQ frames are handled with `get()`/`first()` and `from_utf8_lossy` (no indexing or UTF-8 `expect`), stats signing returns an error that is logged and skipped rather than `expect`-panicking.
**Regression Tests:** `tests/panic_regression_tests.rs` (2 tests).
### 3. Secret Leakage (FIXED)
**Severity:** HIGH | **Status:** Fixed
**Location:** `make_release.sh`, `.gitignore`
**Description:** `make_release.sh` now loads `TOKEN` from `.env` (`.env.example` provided). Private keys (`.pem`, `.key`) are in `.gitignore`. `generate_keys.sh` sets `chmod 600` on generated keys.
**Mitigation Applied:** Secrets removed from scripts and repository. `.gitignore` protects `.env`, `*.pem`, `*.key` files.
**Regression Tests:** `tests/secret_leakage_tests.rs` (3 tests).
### 4. Denial of Service (DoS) (FIXED)
**Severity:** HIGH | **Status:** Fixed
**Location:** `src/bin/bal-server.rs` (HTTP), `src/bin/bal-pusher.rs` (ZMQ)
**Description:** All DoS vectors mitigated via actix-web migration.
**Mitigation Applied:**
- Body size limit: `PayloadConfig::default().limit(max_body_size)` via `BAL_SERVER_ACTIX_MAX_BODY_SIZE` (default 1 MiB).
- Rate limiting: `actix-governor` with a token bucket per client IP, with an **independent bucket for each endpoint class** (push txs, searchtx, info/stats, default) so that polling a cheap endpoint cannot exhaust the push-txs budget. Uses `RealIpKeyExtractor` to extract the real client IP from proxy headers.
- Connection limits: `workers(4)` and `max_connections(100)` via `BAL_SERVER_ACTIX_WORKERS`/`MAX_CONNECTIONS`.
- Request timeout: `middleware::from_fn` wraps every handler in `tokio::time::timeout`, returning `408 Request Timeout` when `BAL_SERVER_ACTIX_TIMEOUT_SECS` (default 5) elapses, covering slow request bodies.
- ZMQ timeout: `set_rcvtimeo(5000)` prevents infinite blocking.
- RPC retry: sleep + retry on connection failure instead of panic.
- ZMQ message handling is panic-free: multipart frames are accessed with `get()`/`first()` and topics are decoded with `from_utf8_lossy`, so malformed frames cannot abort the pusher.
- Stats-report signing failures are logged and skipped instead of panicking the pusher when the key is unreadable.
### 5. SSRF / Network Abuse via `reqwest` (FIXED)
**Severity:** MEDIUM | **Status:** Fixed
**Location:** `src/bin/bal-pusher.rs`, `src/validation.rs`
**Description:** URL validation prevents redirecting requests to internal/private IPs.
**Mitigation Applied:** `is_valid_welist_url()` in `src/validation.rs` blocks localhost, loopback, RFC1918, link-local, multicast, unspecified, IPv6 unique-local addresses, and **IPv4-mapped IPv6 addresses** (`::ffff:a.b.c.d`) that route to loopback/private/link-local/unspecified IPv4 targets. HTTPS-only scheme enforced. Note: hostnames are not DNS-resolved before validation, so a domain that resolves to a private address is not caught by this check — restrict `WELIST_SERVER_URL` to trusted values.
**Regression Tests:** `tests/ssrf_tests.rs` (integration) + 11 unit tests in `src/validation.rs`.
### 6. Insecure Database Access (FIXED)
**Severity:** MEDIUM | **Status:** Fixed
**Location:** `src/db.rs`, `src/bin/bal-server.rs`, `src/bin/bal-pusher.rs`
**Description:** Database path validation and WAL mode for concurrent access.
**Mitigation Applied:**
- `open_db()` rejects `..` traversal, forbidden system directories (`/etc`, `/proc`, `/sys`, `/dev`, `/usr`, `/bin`, `/sbin`, `/lib`, `/opt`), symlinks, and non-regular files.
- WAL mode (`PRAGMA journal_mode=WAL`) with retry logic (up to 5 attempts).
- `busy_timeout=5000` for concurrent access.
- `bal-server` uses `Arc<Mutex<Connection>>` for thread-safe shared access.
**Regression Tests:** `tests/db_path_validation.rs` (5 tests).
### 7. ZMQ Authentication and Encryption (OPEN)
**Severity:** MEDIUM | **Status:** Open
**Location:** `src/bin/bal-pusher.rs`
**Description:** ZMQ connection is plaintext TCP. No authentication (ZAP), no encryption (ZMQ_CURVE). If the ZMQ port is exposed, any attacker can subscribe to topics.
**Mitigation (Operational):**
- Bind ZMQ to `127.0.0.1` only.
- Firewall blocks external access to ZMQ ports.
- If public ZMQ is required, use ZMQ_CURVE with public-key cryptography.
### 8. Missing HTTPS / Insecure Server Communication (FIXED)
**Severity:** HIGH | **Status:** Fixed (Infrastructure)
**Location:** Nginx configuration, `contrib/nginx/bal-server.conf`
**Description:** TLS termination via Nginx reverse proxy. The `bal-server` intentionally does not implement TLS.
**Mitigation Applied:**
- Dedicated Nginx template with `listen 443 ssl http2`, Let's Encrypt paths.
- Security headers: `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`.
- `client_max_body_size` matching `BAL_SERVER_ACTIX_MAX_BODY_SIZE`.
- HTTP 80 redirect to HTTPS.
- Deployment checklist ensures no accidental plain HTTP exposure.
### 9. Missing Input Validation (FIXED)
**Severity:** MEDIUM | **Status:** Fixed
**Location:** `src/bin/bal-server.rs`
**Description:** Network, txid, and content validation.
**Mitigation Applied:**
- Network validation: `NETWORKS.contains(&param.as_str())` before processing. Unknown networks return 404.
- Txid validation: `echo_search` requires exactly 64 ASCII hex characters. Non-hex or wrong length returns 400.
- XPub address caching: `get_all_addresses_by_xpub` loads all addresses once per batch (O(1) lookup), eliminating N+1 queries.
- Content-Length: Handled by actix-web `PayloadConfig` size limit.
**Regression Tests:** `tests/input_validation_tests.rs` (4 tests).
### 10. Information Leakage (MITIGATED)
**Severity:** LOW | **Status:** Mitigated
**Location:** Application logs
**Description:** Raw file logging (`valid_txs`/`invalid_txs`) has been removed. `info!`/`warn!` macros may still log txid and details in application logs.
**Mitigation (Operational):** Set production `RUST_LOG` to `warn` or higher. Log files restricted with `chmod 600`.
### 11. `bal-stats.rs.dontcompile` (REMOVED)
**Severity:** LOW | **Status:** Removed
**Description:** The broken HTML report generator file no longer exists in the source tree.
---
## Hardening Recommendations
### System-Level
1. Run as non-root user with systemd hardening (`ProtectSystem=full`, `NoNewPrivileges`, `PrivateDevices`, `MemoryDenyWriteExecute`).
2. Use firewall to block all inbound ports except HTTPS (443) and SSH (22).
3. Use VPN or Tor for `welist` connections if on public network.
4. Run in a container or chroot for isolation.
5. Enable SELinux or AppArmor profiles for the binaries.
6. Use read-only filesystem for the server binary.
### Application-Level
1. **Rate Limiting:** Implemented via `actix-governor` with token-bucket per client IP and a **separate bucket per endpoint class**. `RealIpKeyExtractor` identifies clients behind reverse proxy using `X-Real-IP` / `X-Forwarded-For` headers. Trusted proxy IP configurable via `BAL_SERVER_TRUSTED_PROXY`.
2. **Input Validation:** Network enum check, txid hex validation, body size limits.
3. **Request Timeout:** `tokio::time::timeout` middleware returns `408` when a handler (including body read) exceeds `BAL_SERVER_ACTIX_TIMEOUT_SECS`.
4. **HTTPS:** Via Nginx reverse proxy with Let's Encrypt.
5. **WAL Mode:** Enabled with retry logic for concurrent access.
6. **ZMQ Timeout:** 5-second receive timeout prevents infinite blocking.
7. **Transaction Size Limits:** Configurable via rate limiting and body size.
8. **ZMQ Retry:** Reconnect logic with timeout-based detection.
9. **Fee Limits:** Per-network `fixed_fee` configuration.
10. **Network Limits:** Only known networks accepted (bitcoin, testnet, testnet4, signet, regtest).
11. **Locktime Reasonableness:** Locktime compared against blockchain height and median time with threshold-based distinction.
---
## Regression Test Suite
| Test File | Tests | Coverage |
|-----------|-------|----------|
| `tests/sql_injection_tests.rs` | 3 | Parameterized queries, injection prevention |
| `tests/panic_regression_tests.rs` | 2 | Mutex poisoning recovery, NULL value handling |
| `tests/ssrf_tests.rs` | 4+ | URL validation, internal IP blocking |
| `tests/secret_leakage_tests.rs` | 3 | .gitignore, no tracked secrets, no hardcoded tokens |
| `tests/input_validation_tests.rs` | 4 | Address caching, network validation, txid hex validation |
| `tests/db_path_validation.rs` | 5 | Path traversal, forbidden dirs, symlinks, WAL pragma, valid paths |
| `src/validation.rs` (inline) | 11 | SSRF URL validation unit tests |