fix(security): require browser origin for folder import when desktop gate inactive (#5480)#6073
Conversation
…gate inactive (nexu-io#5480) POST /api/import/folder and POST /api/projects/:id/working-dir previously accepted arbitrary baseDir from any non-browser local process when the desktop auth gate was inactive (non-desktop mode). This allowed unauthenticated directory binding — a local process could import any folder as a project and read/write its contents via the project file API. Fix: when isDesktopAuthGateActive() returns false, require a browser Origin header on these routes. The server.ts origin middleware already validates browser origins, so a non-empty Origin proves the request came from the legitimate web UI. Desktop mode requests are unaffected — they continue through the existing HMAC import token verification. Adds a shared folderImportAuthorizationReason() helper that both routes use, and regression tests covering rejection of non-browser requests and acceptance of browser-origin requests. Closes nexu-io#5480
|
Thanks @CVE-Hunter-Leo — centralizing the auth gate for both folder import paths is the right direction for this security fix. I've requested review from @nettee. One quick PR-body ask before pool review gets deeper: could you add the missing Surface area and Validation details? For this one, Also flagging this for manual QA before merge because it changes a live import path. Nothing needed from you right now — we'll queue QA once the PR is merge-ready. |
| function folderImportAuthorizationReason(req: import('express').Request): string | null { | ||
| if (isDesktopAuthGateActive()) return null; | ||
| const origin = req.headers.origin; | ||
| if (typeof origin !== 'string' || origin.length === 0) { |
There was a problem hiding this comment.
folderImportAuthorizationReason() is treating the presence of an Origin header as proof that the request came from the web UI, but the daemon's own origin model does not give you that guarantee. server.ts explicitly allows non-browser clients when Origin is missing, and isAllowedBrowserOrigin() only checks that a supplied origin string looks like a same-host loopback/LAN origin. A raw local client can therefore send Origin: http://127.0.0.1:<port> (or another allowed loopback/LAN origin) and still reach these routes, so the unauthenticated directory-binding hole remains open even though the new no-Origin case is blocked. Please switch this gate to a credential that a local script cannot forge, such as keeping the existing desktop import-token/HMAC flow for these privileged routes or another explicit daemon-issued secret, and extend the regression coverage to prove that a non-browser request with a forged allowed Origin is rejected.
|
Hey @CVE-Hunter-Leo — @nettee has the current blocker on this head covered: the new Origin-based gate is still forgeable by a raw local client, so the auth hole is not fully closed yet. Please address that review thread first, and when you update the PR body, also add the missing Surface area and Bug fix verification details so the next pass has the full context. |
Summary
POST /api/import/folder and POST /api/projects/:id/working-dir previously accepted arbitrary baseDir from any non-browser local process when the desktop auth gate was inactive. This allowed unauthenticated directory binding (CWE-284/CWE-22).
Before: non-browser clients could bind arbitrary directories without auth
After: when desktop gate inactive, routes require browser Origin header
Desktop mode requests are unaffected - they continue through HMAC import token verification.
Changes
Surface area
POST /api/import/folderandPOST /api/projects/:id/working-dirfolderImportAuthorizationReason()inapps/daemon/src/import-export-routes.tsValidation
pnpm --filter @open-design/daemon test -- folder-import-auth-gate— all 4 integration test cases passCloses #5480