Skip to content

v1.1.0 — security hardening (full-audit remediation)

Choose a tag to compare

@danielsoimu danielsoimu released this 22 Aug 16:10
· 12 commits to main since this release

Security hardening release. A full-surface security audit (2026-08-22, six parallel passes over crypto, codec, standalone runtime, MCP loader, CLI, editor/supply-chain) found no path to forge provenance, compromise a key, or leak a secret — the findings were availability (DoS) and hardening issues. This release fixes all of them: the one High, all nine Medium, and the selected Low findings. Key findings were independently reproduced and verified by a second, separate review before remediation.

Behavioral changes (read before upgrading)

1. verify now rejects malleated (high-s) ECDSA signatures.
For any ECDSA signature (r, s), the twin (r, n − s) also verifies. Accepting both meant every signed capsule had two valid byte-identities — breaking byte-reproducibility, dedup-by-signature, and the planned provenance chaining (prev), where a third party could fork a chain without holding any key. The signer has always emitted canonical low-s (deterministic RFC 6979), so capsules produced by the editor or CLI are unaffected.

Compat note: capsules signed through the legacy Web Crypto fallback path (non-extractable keys, random-k) are high-s with ~50% probability and will now read INVALID. Re-sign them — or normalize the existing signature to its low-s twin (s' = n − s), which needs no private key and preserves the signer.

2. base64url decoding is strictly canonical. Foreign characters, padding, and non-zero trailing bits are rejected in the core decoder, the CLI, the MCP loader, and the standalone runtime. Encoded strings act as identities (trust comparison, key pinning, capsule hashing); canonicality makes one capsule own exactly one name. Capsules produced by our tooling were always canonical.

3. Passphrase floor enforced at derivation. deriveIdentityFromPassphrase now throws below the floor (≥ 16 chars, score ≥ 3) instead of silently deriving a dictionary-attackable key. Previously the floor was editor-UX-only.

Hardening

  • Decompression bombs bounded in CPU, not just memory (the audit's only High): compressed input is rejected above 17 MiB before any inflate work, then slice-fed so the loop actually stops past the 16 MiB output cap — a 512 MB bomb now aborts in ~120 ms instead of inflating fully. The standalone runtime's DecompressionStream path gains the same output cap (it previously had none) — a hostile capsule can no longer freeze or OOM the reader's tab.
  • Standalone capsule CSP: script-src drops 'unsafe-inline' for per-file sha256- hash pins of the two inline scripts. Any future regression that reintroduces a raw script sink fails closed as a console error instead of executing in the recipient's browser.
  • Envelope size capped before any decoding work; two quadratic (ReDoS-class) regexes in the public markdown paths replaced with linear scans.
  • Core sanitizePage now covers header/footer fields (logo, tagline, copyright, footer links through the URL allowlist); style.bg/fg re-validated against the hex pattern at the CSSOM sink.
  • CLI: ~/.altweb created 0700, identity.json written with mode 0600 at creation (no world-readable window); terminal-escape (ANSI/OSC) stripping on human-facing output so capsule content cannot repaint a verify verdict.
  • MCP SSRF filter now parses IPv6 numerically: uncompressed literals (0:0:0:0:0:0:0:1), NAT64 (64:ff9b::/96), 6to4 (2002::/16), and reserved ::/96 are all refused; v4-mapped addresses are judged on the embedded address.
  • Editor zeroes the signing passphrase from state right after identity unlock.

Verification

137 tests across core/MCP/editor (114 in core, three new suites), including: the actual embedded runtime executed with the real DOMPurify (previously stubbed in every test), CSP hashes recomputed independently from the emitted bytes, malleated-signature and decompression-bomb regressions with machine-relative timing bounds.

npm: altweb@1.1.0 · altweb-context@1.1.0