Harden privacy boundary and asset serving (v0.1.0:74)
Fixes from the 2026-06-12 full-eval (P0 + two P1s); code-only, no schema change. Without these the "private CRM" premise was breachable on the LAN: - P0: the /assets/ route joined the request path onto FRONTEND_DIR without normalizing '..' (get_path/urlparse pass it through), so an unauthenticated GET /assets/../../data/crm.db read any file the process could — the LP DB, the JWT signing secret (-> admin-token forgery), the Gmail key. Add a realpath containment check that 404s anything resolving outside FRONTEND_ROOT. - P1: the LP-outreach drafter built its redaction Boundary with no ner_fn, so unknown people/firms in raw email bodies reached Claude in the clear. Pass the local-Qwen NER backstop (ner_fn=_ner_local), matching architect_grounding; fails closed via the existing scrub_unavailable path if the local model is down. - P1: get-by-id handlers leaked soft-deleted records by direct ID. Add deleted_at IS NULL to every get-by-id path — contacts, organizations, opportunities, lp_profiles — and to the nested related-data sub-selects in the contact/opportunity detail payloads, matching the list-handler convention. Bumps the package to v0.1.0:74 (utils.ts + versions/v0.1.0.74.ts + graph). Full report in EVALUATION.md; remaining P2/P3 triaged in AGENTS.md Current state.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { VersionInfo } from '@start9labs/start-sdk'
|
||||
|
||||
// Security/privacy hardening from the 2026-06-12 full-eval (P0 + two P1s). Code-only,
|
||||
// no schema change (migrations are no-ops):
|
||||
// * P0 — pre-auth path traversal in the /assets/ route (server.py): get_path()/urlparse
|
||||
// does not normalize '..', so an unauthenticated GET /assets/../../data/crm.db (raw
|
||||
// client) read any file the process could — the LP DB, the JWT signing secret (-> admin
|
||||
// token forgery), the Gmail service-account key. Added a realpath containment check that
|
||||
// 404s anything resolving outside FRONTEND_DIR.
|
||||
// * P1 — the LP-outreach drafter (mcp/outreach_agent.py) built its redaction Boundary with
|
||||
// no ner_fn, so unknown people/firms in raw email bodies reached Claude in the clear.
|
||||
// Now passes the local-Qwen NER backstop (ner_fn=_ner_local) like architect_grounding;
|
||||
// fails closed via the existing scrub_unavailable path if the local model is down.
|
||||
// * P1 — get-by-ID handlers for contacts and organizations (server.py) omitted the
|
||||
// deleted_at IS NULL filter, so soft-deleted records stayed readable by direct ID.
|
||||
export const v_0_1_0_74 = VersionInfo.of({
|
||||
version: '0.1.0:74',
|
||||
releaseNotes: {
|
||||
en_US: [
|
||||
'Security hardening: close an unauthenticated file-read in static-asset serving (could expose',
|
||||
'the database, the auth secret, and the Gmail key), tighten the LP-outreach privacy boundary so',
|
||||
'unknown names in email bodies are de-identified before reaching Claude, and stop soft-deleted',
|
||||
'contacts and organizations from being readable by direct link.',
|
||||
].join(' '),
|
||||
},
|
||||
migrations: { up: async () => {}, down: async () => {} },
|
||||
})
|
||||
Reference in New Issue
Block a user