Skip to content

fix: boot-bound request signatures, per-client limits behind a proxy, and a stricter gate (review E-01, E-02, E-03, E-06, E-07) - #38

Merged
HackTuah merged 7 commits into
mainfrom
fix/sec-e-server
Oct 7, 2026
Merged

HackTuah merged 7 commits into
mainfrom
fix/sec-e-server

Conversation

@HackTuah

@HackTuah HackTuah commented Oct 7, 2026

Copy link
Copy Markdown
Member

Fixes E-01, E-02, E-03, E-06 and E-07 from reviewer E on v1.0.0-rc.3 (slice 54, SCR-476). Each comes with regression tests; the builder replayed reviewer E's proofs against live servers built from the old code and from this branch.

  • E-01: a restart can't let a captured request replay. The server makes a random boot id at each start. The proof answer returns it with a MAC (boot_mac), and the extension signs it into every request (Kotiko-HMAC v2 … boot=…). A request carrying an old boot id gets 401 stale_boot, so the extension proves again and retries once. Timestamps more than 120 s in the future are refused too. Reviewer E's replay after a restart: before, +30/+90/+119 s answered 200 with the word list; now every case gets 401 stale_boot. New test vectors are in http-api.md, computed independently in Elixir and JS.
  • E-02: clients behind a reverse proxy. More headers count as forwarded: Via, X-Client-IP, X-Cluster-Client-IP, Fastly-Client-IP, X-Original-Forwarded-For, any X-Forwarded-* and others. New opt-in TRUSTED_PROXY_HEADER (x-forwarded-for rightmost, x-real-ip, cf-connecting-ip or forwarded) counts each proxied client separately, so a stranger can't lock out the owner's other devices. The docs that said "never the extension" are corrected.
  • E-03: a key in a provider URL's query is kept out of the log: the startup summary shows ?…, and the query values go to the log redactor. Queries still work, so ?api-version= addresses are fine, and the model client now puts the path before the query.
  • E-06: the store gate reads lines as a reader sees them. HTML comments, tags, entities, code fences and table cells are all handled, and the appendix heading counts only when it is visible. All 15 of reviewer E's disguises now stay open; the control closes. The lead checked that the draft review report, closed, passes (--dir).
  • E-07: the open routes answer only the exact /health and POST /api/v1/proof.

Compatibility: an rc.3 extension and this server don't talk (malformed), and this extension with an rc.3 server asks you to update the server. Update both together.

Checks: format, compile with warnings as errors, credo strict, mix test 1205 tests and 23 properties with 0 failures, Sobelow; lint; coverage 94.39 % lines, 85.04 % branches; Playwright loopback, fullstack, connect and security 21/21; REUSE; DCO.

🤖 Generated with Claude Code

Ayla Croft and others added 5 commits October 7, 2026 15:31
Security review E-07. The router let a request through without the token
when its path_info was ["health"] or ["api", "v1", "proof"]. path_info drops
empty segments, so //api/v1/proof, /api/v1/proof/ and GET /health/ were
open too, although the HTTP API reference says only the exact paths are.
The check now compares the raw request path, so every other spelling needs
the token (401), and a test sends each spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
Security review E-03. Some providers take their key in the query
(?key=...), and the start-up summary logged LLM_URL whole at info. The
summary now shows a service address without user name, password, query or
fragment (https://host/v1?…), and the values in the queries of LLM_URL and
TRANSCRIBE_URL, as written and decoded, join the secrets the log filter
takes out of every line. Queries are still allowed (some providers need
?api-version=), and the model client now puts the request path before the
query instead of after it, so such an address works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
…headers

Security review E-02. Requests a reverse proxy on this computer forwards
were counted together for the wrong-token lockout and the proof limit, so a
stranger sending ten wrong tokens a minute kept the owner's other devices
out for as long as they kept at it. And a proxy that added only a header
outside the list (Via, X-Client-IP, X-Cluster-Client-IP, Fastly-Client-IP,
X-Original-Forwarded-For, ...) made its clients look like this computer's
own, which are never limited.

The list now covers those headers, any X-Forwarded-* and Tailscale's
Tailscale-User-*. The new opt-in setting TRUSTED_PROXY_HEADER
(x-forwarded-for, x-real-ip, cf-connecting-ip or forwarded; checked at
start) names the header the proxy sets; each proxied client is then counted
by that header's address (the rightmost X-Forwarded-For entry, the last
Forwarded for=), normalised like a peer. Unset, the shared count stays. The
reference no longer says the extension is never locked out behind a proxy:
only the extension on this computer is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
…t replay

Security review E-01. The server remembers the nonces of signed requests
only in memory, and refused only a ts from before its start. So a request a
program caught while the server was stopped could be played to the server
after a restart when it was stamped ahead of the server's clock (a client
clock up to 119 s fast, or the start second): reviewer E got 200 and the
learner's words with ts +30, +90 and +119 s.

The server now makes a random boot id (16 bytes, base64url) at each start.
The proof answer carries it with boot_mac = HMAC(token, "kotiko-boot-v1\n"
+ nonce + "\n" + boot), and every signed request signs it: the header is
now Kotiko-HMAC v2 with a boot= field, and the canonical request
(kotiko-req-v2) has the boot id between the nonce and the body hash. A
request for another boot id gets 401 error="stale_boot" (not counted as a
guess, since its MAC matched); the extension then proves again and sends the
request once more. The window stays 120 s either way; the start-time check
is gone, since the boot id covers it without refusing a slightly slow
client clock just after a start. The HTTP reference has the new scheme, the
clock requirement and new vectors, checked by the server's and the
extension's tests; a full-stack test replays reviewer E's scenario on the
real server across a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
Security review E-06. The store gate still read 14 disguised open lines
beside the exact closed line as closed: "1. Gate: open", "| Gate | open |",
inline code, "~Gate: open~", an emoji or arrow before it, "<b>Gate</b>:",
"G&#97;te:", "$Gate$:", a fake "## Appendix" inside an HTML comment or a
code fence (so the open line below it counted for nothing), a heading
followed by a zero-width character, a closed line only inside a comment,
and "Status: the review is still OPEN" prose.

A line now counts as a gate line if any reading of it, or of it joined with
the next line, has the gate word (lookalikes as before) followed within a
few non-letters by a colon-like or table-cell separator. The readings take
out HTML comments (across lines too), tags and entities, and normalise as
before; text in comments and code still counts, the safe side. Prose that
says "open" beside "gate", "review" or "status" keeps the gate open. The
appendix heading is the first visible one in plain ASCII, and the closed
line must be visible (not in a comment, code fence or HTML block) and in a
paragraph without strikethrough, HTML, math, code or entity markup. Reviewer
D's cases and the earlier tests keep their results; reviewer E's sixteen
are in the unit tests, with more of the same kind and a report shaped like
the real one that must still close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
@HackTuah
HackTuah requested a review from minitru as a code owner October 7, 2026 19:57
Comment thread scripts/check-security-gate.mjs Fixed
Ayla Croft and others added 2 commits October 7, 2026 16:08
CodeQL (js/incomplete-multi-character-sanitization) flagged the single
pass that removes HTML tags before reading a gate line: "<<b>b>" leaves
"<b>". The script only reads report text and never renders it, but a
tag left over could hide a gate word from the check, so the removal now
repeats until the line stops changing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
@HackTuah
HackTuah merged commit 1eb692a into main Oct 7, 2026
14 checks passed
@HackTuah
HackTuah deleted the fix/sec-e-server branch October 7, 2026 20:41
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.

2 participants