Auth gate is bypassable by percent-encoding the path #120
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lz/agent-nexus#120
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Unauthenticated bypass of the entire API auth gate. Pre-existing — not introduced by #109, but found while reviewing it. Verified live against
vite devon currentmain-equivalent code.Reproduction
%61isa. No cookie, no token, no credential of any kind.Not read-only:
Confirmed variants:
/%61pi/…,/a%70i/…,/ap%69/…,/%61%70%69/….Cause
hooks.server.tsdecides admission onevent.url.pathname, which SvelteKit leaves percent-encoded exactly as sent. The router matches on the decoded path:respond.js:254—resolved_path = decode_pathname(resolved_path), wheredecode_pathnameispathname.split('%25').map(decodeURI).join('%25')(utils/url.js:50)respond.js:322—find_route(resolved_path, …)populatesevent.route.idurl.pathnameis never rewritten. The only pre-handlenormalisation is the trailing-slash 308 atrespond.js:366, which does not touch percent-encoding.So
path.startsWith('/api/')tests a string the router has already discarded./%61pi/quotafails the prefix test, hitsreturn resolve(event)unconditionally, and the real/api/quotahandler runs.Versions: SvelteKit 2.59.1.
Blast radius
Every API route that does not separately guard on
locals.masterKey— around 30. The ones that matter most, all reachable unauthenticated:PUT /api/settings/agent-instructions— rewrites the fleet-shared~/.claude/CLAUDE.md, which every running agent live-readsPOST /api/previews/[previewId]/approve— the operator-consent gate for opening a host portDELETE /api/workers/[id],POST /api/workers/[id]/stop,POST /api/workers/[id]/sessionsGET /api/workers/[id]/logs,GET …/artifacts/[artifactId]/rawPUT /api/settings/instanceConfig-explorer, connections and files routes survive only because each re-checks
masterKey. The defense-in-depth described in that guard's docstring is currently doing the entire job on its own.Fix
Decide admission on
event.route.id— the identifier the router actually resolved — rather than onevent.url.pathname. The two cannot then disagree by construction.PUBLIC_APIbecomes a set of route ids; for the four static routes it contains (/api/state,/api/auth/setup,/api/auth/unlock,/api/auth/lock) the strings are identical, so it is a same-shape change.A null
route.idmeans the router matched nothing, so there is no handler to protect and the request 404s regardless.This is the argument
tokens/scopes.tsalready makes for the scope map — that keying onroute.idavoids "the path-normalisation surface". The gate needs to follow its own advice.Note on exposure
preview_bind_interfaceandpublic_urlgovern how reachable an instance is. Any deployment reachable beyond loopback should treat this as urgent. See also #110.