Repository navigation
fix(security): one request guard for the dashboard — no write from another site, no other host name - #152
Open
UltimaPhoenix wants to merge 1 commit into
Open
fix(security): one request guard for the dashboard — no write from another site, no other host name#152UltimaPhoenix wants to merge 1 commit into
UltimaPhoenix wants to merge 1 commit into
Conversation
…ther site, no other host name Several dashboard routes that write had no cross-site check: /settings, /settings/notebook, /settings/import, /knowledge*, /groups*, star and feedback. While `devcoach ui` ran, any web page could submit a form to them — e.g. rewrite the notebook the agent reads through get_briefing, or change the pacing. The docs share page makes it concrete: once Chrome has been allowed local network access for the probe, any script on that origin can POST to 127.0.0.1 (and a top-level form POST needs no permission at all). One `app.use` now runs before every route (`refusal`): - a method other than GET/HEAD/OPTIONS whose Sec-Fetch-Site is not same-origin/none → 403; - a Host other than 127.0.0.1 / localhost / [::1] → 403, reads included (DNS rebinding would otherwise make a foreign page same-origin with the dashboard and bypass the first check). The share page keeps working: it only reads (GET /ping + its OPTIONS preflight, the link to the GET /lessons/import preview); the lesson is saved by the preview's own same-origin POST. The per-route checks on writes go away, and with them the import's "cross" redirect, which no devcoach surface can reach. The share route keeps its own check for cross-site GETs. Tests loop over app.routes so a new write route cannot miss the guard, cover the share page's reads, the rebinding names, and a real socket with a forged Host header.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
While
devcoach uiruns, any web page could change the dashboard: several routes that write had no cross-site check —/settings,/settings/notebook,/settings/import,/knowledge*,/groups*,/lessons/:id/star,/lessons/:id/feedback. A page could, for example, rewrite the coaching notebook the agent reads throughget_briefingbefore every lesson, or change the pacing (nudge_every,max_per_day).The docs share page makes it concrete: once Chrome has been allowed Local network access for the dashboard probe, any script on
https://ultimaphoenix.github.io(every Pages site of the account shares that origin) can reach127.0.0.1— and a top-level form POST needs no permission at all.One
app.usenow runs before every route (refusalinsrc/web/app.ts):Sec-Fetch-Siteis notsame-origin/none(same-siteincluded: another server on 127.0.0.1) → 403.Hostother than127.0.0.1/localhost/[::1]→ 403, reads included. Without it, DNS rebinding makes a foreign page same-origin with the dashboard and walks past the first check (and reads/settings/export, the notebook, the lessons).Before / after
Slideshow — each screen BEFORE (red band: without this PR), then AFTER (green band: with this PR), with one line saying what changed. It plays by itself; click it to open it full size.
Dark
Side by side — BEFORE on the left, AFTER on the right.
A form on another site posts to your dashboard: before 200, the write goes through; after 403 Forbidden.
Dark
Your notebook right after: before rewritten by the other site; after unchanged.
Dark
The share page's own import: saved by your own click, before and after.
Dark
Videos — the first frame is the title; a full red BEFORE or green AFTER card opens every segment, and the band in the same colour stays above the recording until the segment ends.
Light
pr-129-before-after-light.mp4
Dark
pr-129-before-after-dark.mp4
Same script both times, against a sandbox dashboard (screenshot fixture, never a real
~/.devcoach), in Chrome 156 with local network access allowed for the docs site:/settings/notebookWhy the share page keeps working
It only reads:
GET /ping(+ itsOPTIONSpreflight, still CORS-locked to the docs origin) and the link to theGET /lessons/import?code=preview. The lesson is saved by the preview's own same-origin POST.Also
error=crossredirect, which no devcoach surface can reach. The share route keeps its own check for cross-site GETs.docs/reference/privacy.md(The dashboard answers only itself), CLAUDE.md.Test plan
npm run lint && npm run typecheck && npm run test:cov— 389 tests pass; coverage 94.83 % lines · 93.60 % statements · 96.84 % functions · 84.18 % branches (thresholds 92 / 91 / 95 / 76)web request guardsuite: loops overapp.routes(a future write route cannot miss the guard) fromcross-siteandsame-sitewith the docsOrigin— notebook, settings, knowledge unchanged; the share page's reads (/ping, preflight, preview, then the same-origin Add) pass; rebinding names refused for reads and writes; a real socket with a forgedHostheader🤖 Generated with Claude Code