Skip to content

029 — the threat model, package-wide; JSON nesting bounded before the decoder on both transports - #40

Open
HackTuah wants to merge 3 commits into
mainfrom
slice/029-threat-model
Open

HackTuah wants to merge 3 commits into
mainfrom
slice/029-threat-model

Conversation

@HackTuah

Copy link
Copy Markdown
Member

The package-wide threat model, and the one wire bound it found missing.

docs/threat-model.md. Who is trusted for what — the host for everything, the client on the wire for nothing, the node for everything by physics, this package for holding no tool, key, signature, session, authority or client. Then the wire, vector by vector, each refused, bounded, or delegated to the HTTP server or the host by decision, with the test that enforces it by path and by name and the OWASP id it answers to (the December 2025 Top 10 for Agentic Applications; the 2025 LLM Top 10 by edition — the ids were read from the project's pages, not recalled). It extends the tracer's model shipped in 0.4.0: the node-local adversary stays out of scope, now for every module, with the reason. Prompt injection through tool results is the host's (LLM01:2025) — this package carries bytes and never reads them. A federation section states the trust domains a merge would cross — attribution, identity, signs, integrity in transit, the peer as a client — so the seam is designed against them rather than discovering them. Where the Plug goes in a host's pipeline: ahead of Plug.Parsers, or its path excluded — behind the parsers every request is -32700 Parse error: empty body (measured). A census holds every citation on the page to a test in the tree, by the reader the will-not-implement census now shares; it caught three misquoted names while the page was being written.

JSON nesting is bounded before the decoder runs, on both transports. Measured first: the 1 MiB body cap bounds how deep a body can nest but not what decoding it costs — a 1 MiB body nested 524,288 levels deep was decoded in full, 79–96 ms and a 38 MiB heap for one request, about 36× the body, and refused only afterwards by its shape. BeamMCP.JSON.decode/1 is now the one place the wire's JSON is read: a byte walk in front of the decoder (brackets outside strings, escapes honoured) refuses a body nesting past 64 levels with -32600 "Request body nests deeper than 64 levels" — 400 over HTTP with the connection kept, the same object on stdio — building nothing. The worst body under the cap is refused in microseconds; a request-sized body pays nothing measurable; a 1 MiB well-formed one +1.7 ms. Red first (six tests failing on behaviour with the constant present and nothing enforcing it), then green; ten mutants over the walk and the two transport clauses, zero survivors, every kill a named test. Only a client nesting past 64 levels sees a change; no field, method or capability moves — the conformance rows are unchanged (16/37, 5/6; 0/30 by design).

Gate on the head: thirteen pass, GATE_EXIT=0. 11 properties, 611 tests, 0 failures — 604 at v0.5.0 plus the seven of the threat-model test. The package-reach census refused a Jason.DecodeError named in a @spec on the way (struct() instead) — the census working as built.

…nsports, and docs/threat-model.md cites a test per refusal -- measured first: a 1 MiB body nested 524 288 deep is decoded in full today (79-96 ms, a 38 MiB heap, ~36x the body) and refused afterwards by shape; the test file, a constant-only BeamMCP.JSON scaffold, the page-citation reader moved into the Boundary support module for both censuses: 6 tests, 5 failures (a nest past the bound answered 200 over HTTP and decoded over stdio, the worst body's heap, the page absent)

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…oth transports, a byte walk in front of the decoder refusing a body nested past 64 levels by name (-32600; 400 with the connection kept over HTTP, the same object on stdio) and building nothing: the worst 1 MiB body is refused in microseconds where it had cost a 38 MiB heap, a request-sized body pays nothing measurable, a 1 MiB well-formed one +1.7 ms; docs/threat-model.md -- who is trusted for what, the wire vector by vector as refused, bounded or delegated with the test per row and the OWASP ids read from the source (ASI December 2025, LLM 2025 by edition; the plan's LLM03 for prompt injection is LLM01:2025), the node-local adversary out for every module, the federation trust domains, the Plug.Parsers placement measured; the page's citations held to the tree by the reader the will-not-implement census now shares; ExDoc extra under Policy, README pointers, the observed page's pointer, a CHANGELOG entry with the how-to-tell; the package-reach census refused a Jason.DecodeError named in a spec -- struct() instead

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…-- the walk ignored, off by one, objects uncounted, no count-down, strings not skipped, escapes not honoured, the wrong depth named, the HTTP refusal as a parse error, the connection closed on it, stdio's clause removed -- zero survivors, every kill a named test; two re-anchored so an orphaned function is not the kill

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant