security: enforce max_send_lovelace + sandbox *_path args
Two CRIT findings from the 2026-05-12 Opus audit. Both are mainnet-blocking against the aldabra-mainnet container. CRIT-1 — cap-bypass via unsigned-build → sign_partial → submit chain. Previously `wallet_send` / `wallet_mint` / `wallet_mint_cip68_nft` / `wallet_script_spend` enforced `max_send_lovelace`, but the unsigned- build tools + `wallet_sign_partial` + `wallet_submit_signed_tx` did not. A prompt-injection that walked the cold-signer chain could drain the wallet past the cap with zero policy enforcement. Fix: - `wallet_send_unsigned` / `wallet_mint_unsigned` / `wallet_plutus_mint_unsigned` now enforce the cap on the user- supplied destination lovelace, mirroring their signed equivalents. All three gain a `force: bool` arg with `#[serde(default)]`. - `wallet_sign_partial` and `wallet_submit_signed_tx` decode the Conway tx CBOR, sum lovelace across every output whose address is NOT this wallet's own primary address, and enforce the cap on that total. Both gain `force: bool`. The chokepoint covers cold-signed multi-sig flows and any hand-built CBOR the daemon would otherwise blindly sign or submit. - New free fn `sum_non_self_lovelace` is the unit-testable core of the chokepoint logic; `enforce_cap_on_cbor` wraps it. - The sum uses `try_fold` + `checked_add` (NOT `.sum::<u64>()`) so a crafted CBOR that overflows `u64::MAX` fails the check instead of wrapping silently in release builds. CRIT-2 — path traversal via `reference_script_path` and `policy_cbor_path`. Previously the tools called `std::fs::read_to_ string(p)` on any path the LLM passed. The MCP daemon runs as the same user that owns `$ALDABRA_DATA/mnemonic.age` / `$ALDABRA_DATA/root-xprv.age`. Decode-error messages included the hex_decode position offset — a small but real information leak about non-hex file structure. Fix: - New `Config::safe_reads_root` field (default `$ALDABRA_DATA/scripts/`, override via `ALDABRA_SAFE_READS_ROOT` env or TOML). - New `assert_inside_sandbox` helper canonicalize()s both the root and the user-supplied path, then enforces `starts_with`. Rejects outside-root paths, `..`-traversal, and nonexistent paths with generic messages. - Hardlink-rejection: post-canonicalize, stat the file and refuse if `nlink > 1`. `canonicalize` resolves symlinks but NOT hardlinks (a hardlink IS the file — same inode, different directory entry), so without this check an attacker with daemon-uid write access could plant a hardlink to the encrypted key blob inside the sandbox and exfiltrate bytes through the read path. - `resolve_ref_script_bytes` + `resolve_policy_cbor_bytes` + the `resolve_validator_required` wrapper used by all 5 escrow spend tools take `&Path` and route through the sandbox. - Error messages on hex_decode failures no longer carry the path string or byte-offset position — return a constant "contents are not valid hex" instead. - `main.rs` creates the sandbox root with 0o700 perms at startup if missing. chmod errors are surfaced (not swallowed) so a broken filesystem doesn't silently fall back to umask 0o755. - README documents the new `ALDABRA_SAFE_READS_ROOT` env var alongside `ALDABRA_MAX_SEND_LOVELACE` (also previously undocumented). Tests (243 → 253, +10): - 5 sandbox tests: accept-inside, reject-outside, reject-dotdot, reject-nonexistent, reject-hardlink. - 1 non-hex regression: constant message (no byte-offset leak). - 3 cap tests: self-send → 0 non-self total, outbound counts, overflow → Err (regression for the prompt-injection `u64::MAX` wraparound attempt). - 1 garbage-CBOR test: clean error. No new clippy warnings, no new fmt drift, `cargo audit` unchanged (0 CVEs, 2 transitive unmaintained warnings). Adversarial review of the first draft (3 Opus reviewers) caught the u64 overflow, the hardlink bypass, and the swallowed chmod error.
This commit is contained in:
parent
45954f3f75
commit
d1c9e7a732
6 changed files with 567 additions and 20 deletions
|
|
@ -100,6 +100,8 @@ Environment variables consumed at startup:
|
|||
| `ALDABRA_KOIOS_BASE` | no | public Koios for the chosen network | Override to point at a self-hosted Koios. |
|
||||
| `ALDABRA_PASSPHRASE` | yes | — | Unlocks `mnemonic.age`. Source from a docker secret or systemd `EnvironmentFile` — never commit it. |
|
||||
| `ALDABRA_BOOTSTRAP` | no | (unset) | Set to `new` or `import` to enter bootstrap mode on next launch. |
|
||||
| `ALDABRA_MAX_SEND_LOVELACE` | no | mainnet 10 ADA / preprod\|preview 100 tADA | Hard cap on lovelace flowing to any non-self destination. Enforced by every tool that signs or submits; pass `force=true` per-tool to override. |
|
||||
| `ALDABRA_SAFE_READS_ROOT` | no | `$ALDABRA_DATA/scripts/` | Sandbox dir for the `reference_script_path` / `policy_cbor_path` tool args. Files outside this dir (canonical) are refused. Created with 0o700 at startup if missing. |
|
||||
|
||||
## MCP tools
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue