Repository navigation
fix(core): Stop /health revealing the home path - #309
Merged
Merged
Conversation
- GET /health sent the helper's script path (which holds the username) and pid to Origin null, which any website can send from a sandboxed frame. Requests with an Origin now get only ok and protocol, the fields the page reads - The script path and pid go only to requests with no Origin addressed to a loopback host, which is what --ensure sends; a DNS-rebound page sends no Origin on a GET but names its own host - Refresh errors name the file as the request gave it rather than a resolved symlink target - Security tests for each origin, a rebound Host, the symlink error, and --ensure replacing a running helper from another install - SKILL.md threat model covers /health; closedloop-core 1.5.1 Testing: pytest plugins/ (2430 passed), ruff, pyright, check_graph.cjs on freshly rendered pages, curl against a scratch helper on port 8799, and each new test checked against a reverted or over-trimmed helper Risks: A helper older than 1.5.0 that only reports script to browsers is unaffected; --ensure still falls back to lsof when pid is missing
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.
Purpose
#308 (closedloop-core 1.5.0) meant to stop the
prd-dependency-graphrefresh helper from revealing the user's home path, and with it their username, to websites.GET /healthstill did. It returned the helper's script path and pid to any request whose Origin wasnullor loopback. #308's own threat model says Originnullcan come from a sandboxed frame on any website, so any site could read the username.Reproduction (running 1.5.0 helper)
Fix
--ensurepath (ensure_helperandhelper_health, run from Python with no Origin) readsscriptandpid, to recognize and replace a helper from another install. The page reads onlyprotocolfrom/health(render_graph.py,unreachable()), to tell an out-of-date helper from a missing one./healthnow answers any request that carries an Origin (null, loopback, or any other) with{"ok": true, "protocol": 2}only. CORS is unchanged:nulland loopback origins can still read it, other origins cannot.scriptandpidare added only when there is no Origin and the Host header is127.0.0.1,localhostor[::1]. The Host check covers DNS rebinding: a rebound page's same-origin GET carries no Origin, but its Host is the attacker's name.Sweep for other paths sent to a browser
POST /v2/refreshfrom a file page already answers only{ok, written, refresh_id}, with no path (pinned by an existing test)./refresh) errors are already generic and name no paths (existing test). The 500 handlers on both endpoints send only~/.local/share/...as a hint.prd-graph-helperor~/.local/bin/prd-graph-helper, never an absolute path (existing test asserts the page has no home path).refresh_file's errors namedreal.name, the resolved symlink target's file name, which the requester never sent. For example, a guessed/.../guess.htmlsymlinked to<user>-private-notes.htmlanswered<user>-private-notes.html holds no refreshable graph.Errors now use the name the request gave.This graph still refreshes from <config path>message is reachable only through--apply-configon the command line, never over HTTP.Files
plugins/closedloop-core/skills/prd-dependency-graph/scripts/refresh_server.py:/healthfield gating (_from_local_program), error file names inrefresh_fileplugins/closedloop-core/tools/python/test_prd_dependency_graph_security.py: new tests (below), replacing the old/healthtest that checked onlyprotocoland CORSplugins/closedloop-core/tools/python/conftest.py: thehelperrequest function takes an optionalhostplugins/closedloop-core/skills/prd-dependency-graph/SKILL.md: Security section covers/health, the Host check and error file namesplugins/closedloop-core/.claude-plugin/plugin.json,.codex-plugin/plugin.json: 1.5.0 to 1.5.1CHANGELOG.md: closedloop-core v1.5.1, FixedTests
/healthwith Originnull,http://localhost:5173,http://127.0.0.1:8000andhttps://evil.examplereturns exactly{ok, protocol}, with no script directory, pid or real home directory in the body (CORS as before)/healthwith no Origin returnsscriptandpid/healthwith no Origin andHost: evil.example:8765returns noscriptorpidrefresh_server.py --ensurefrom this install recognizes it from/health, replaces it (old process exits, new pid and script), and a second--ensurereports it already running. Both helpers are stopped in teardown.Each was checked against the wrong code:
origin/main: the 4 origin cases, the rebinding test and the symlink test fail (6 failed)script/pidstripped for every request: the no-Origin test and the--ensuretest failCommands run
check_graph.cjs ran with
PLAYWRIGHT_COREset to playwright-core 1.60.0 andCHROME_PATHset to the local Chrome.