Skip to content

Require an access boundary before exposing SparseTree beyond loopback #169

Description

@atomantic

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

  1. Extract app construction from listener startup so access middleware is testable without binding a port.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  • Startup on a non-loopback host fails clearly when SPARSETREE_API_TOKEN is absent.
  • In external mode, every mounted API and AI Toolkit route rejects absent and wrong bearer tokens before reaching handlers.
  • A valid token can use the intended API routes without being logged or reflected.
  • The browser token is available to server-side refresh/index jobs but no HTTP endpoint returns it.
  • Loopback-only development continues to work without forcing a token, and CORS remains an explicit origin allowlist.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort:highHigh reasoning effort recommendedmodel:mediumSuggested medium-capability modelplanClaimable backlog itemsecuritySecurity audit findingseverity:highHigh severity

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions