Problem
SparseTree has no authentication or network-exposure guard on its API surface. Its default bind is loopback, but HOST is an unrestricted environment variable and the server mounts all control-plane and data routes before doing any access check. CORS is configured from CORS_ORIGIN, but CORS only constrains browser JavaScript; it does not authenticate direct HTTP clients on a LAN, tailnet, or reverse-proxy deployment.
Evidence
server/src/index.ts accepts an arbitrary deployment host and starts the HTTP listener with it:
const HOST = process.env.HOST || 'localhost';
httpServer.listen(PORT, HOST, () => {
Before that listener is started, the process applies CORS and mounts every API router without an authentication middleware:
app.use(cors({ origin: corsOrigin }));
app.use(express.json());
...
app.use('/api/browser', browserRouter);
app.use('/api/scrape-providers', providerRouter);
The browser router then returns the live FamilySearch session token to any request when a browser is connected:
router.get('/token', async (_req: Request, res: Response) => {
...
res.json({ success: true, data: { token: result.token, cookieCount: result.cookies.length } });
});
The same unauthenticated mount permits credential replacement and deletion (router.post('/:provider/credentials' and router.delete('/:provider/credentials')). The token route has no client caller; server-side refresh/indexing code calls browserService.getFamilySearchToken() directly.
Impact
If an operator sets HOST=0.0.0.0, a tailnet address, or places the service behind a proxy, any network client that can reach the port can retrieve an authenticated FamilySearch token, mutate or erase stored provider credentials, drive the connected browser, and read or modify private genealogy data. A malicious web origin does not need CORS permission when it uses a native client, a proxy, or an already permitted origin.
Implementation plan
- Extract app construction from listener startup so access middleware is testable without binding a port.
- Treat loopback-only binding (
localhost, 127.0.0.1, and ::1) as the local-development mode. Reject a non-loopback HOST at startup unless a dedicated non-empty SPARSETREE_API_TOKEN is configured; document the requirement and never log the token.
- Add a constant-time bearer-token middleware before AI Toolkit and every
/api mount when external mode is enabled. Return 401 for missing or malformed credentials and 403 for invalid credentials; do not rely on Origin or CORS as authorization. Keep the health endpoint intentionally unauthenticated only if it reveals no user data, otherwise protect it too.
- Remove
GET /api/browser/token rather than shipping session credentials over HTTP. Preserve internal callers through browserService.getFamilySearchToken() and return a documented 410 for one release only if backward compatibility for undocumented consumers is required.
- Add explicit CORS allowlist validation for external mode, disable credentialed cross-origin requests unless a future authenticated browser client requires them, and document reverse-proxy/Tailscale deployment expectations.
Acceptance criteria
Verification
Add Supertest coverage for loopback and external configurations: assert startup refusal, 401/403 before router handlers, valid-token success, and the absence/410 behavior of /api/browser/token. Run npm test -- --run, npm run build, and npm audit --omit=dev --package-lock-only.
Dependencies and related work
None. This is independent of the staged JSON/SQLite/PostgreSQL migration issues (#120 and #149-#155).
Scope
Complexity: Medium. Likely files: server/src/index.ts, a new access-control middleware/config module, server/src/routes/browser.routes.ts, deployment documentation, and focused integration tests. Non-goals: adding multi-user accounts, changing credential-at-rest encryption, or changing genealogy-provider behavior.
Problem
SparseTree has no authentication or network-exposure guard on its API surface. Its default bind is loopback, but
HOSTis an unrestricted environment variable and the server mounts all control-plane and data routes before doing any access check. CORS is configured fromCORS_ORIGIN, but CORS only constrains browser JavaScript; it does not authenticate direct HTTP clients on a LAN, tailnet, or reverse-proxy deployment.Evidence
server/src/index.tsaccepts an arbitrary deployment host and starts the HTTP listener with it:Before that listener is started, the process applies CORS and mounts every API router without an authentication middleware:
The browser router then returns the live FamilySearch session token to any request when a browser is connected:
The same unauthenticated mount permits credential replacement and deletion (
router.post('/:provider/credentials'androuter.delete('/:provider/credentials')). The token route has no client caller; server-side refresh/indexing code callsbrowserService.getFamilySearchToken()directly.Impact
If an operator sets
HOST=0.0.0.0, a tailnet address, or places the service behind a proxy, any network client that can reach the port can retrieve an authenticated FamilySearch token, mutate or erase stored provider credentials, drive the connected browser, and read or modify private genealogy data. A malicious web origin does not need CORS permission when it uses a native client, a proxy, or an already permitted origin.Implementation plan
localhost,127.0.0.1, and::1) as the local-development mode. Reject a non-loopbackHOSTat startup unless a dedicated non-emptySPARSETREE_API_TOKENis configured; document the requirement and never log the token./apimount when external mode is enabled. Return401for missing or malformed credentials and403for invalid credentials; do not rely onOriginor CORS as authorization. Keep the health endpoint intentionally unauthenticated only if it reveals no user data, otherwise protect it too.GET /api/browser/tokenrather than shipping session credentials over HTTP. Preserve internal callers throughbrowserService.getFamilySearchToken()and return a documented410for one release only if backward compatibility for undocumented consumers is required.Acceptance criteria
SPARSETREE_API_TOKENis absent.Verification
Add Supertest coverage for loopback and external configurations: assert startup refusal,
401/403before router handlers, valid-token success, and the absence/410behavior of/api/browser/token. Runnpm test -- --run,npm run build, andnpm audit --omit=dev --package-lock-only.Dependencies and related work
None. This is independent of the staged JSON/SQLite/PostgreSQL migration issues (#120 and #149-#155).
Scope
Complexity: Medium. Likely files:
server/src/index.ts, a new access-control middleware/config module,server/src/routes/browser.routes.ts, deployment documentation, and focused integration tests. Non-goals: adding multi-user accounts, changing credential-at-rest encryption, or changing genealogy-provider behavior.