fix(audit): sanitize graph error paths + expose intention status field

Two fixes surfaced by the pre-merge audit of chore/v2.0.7-clean:

1. Security MEDIUM (audit M2): `graph/+page.svelte` was rendering
   `e.message` verbatim into the DOM. A backend error that carried a
   filesystem path (e.g. a wrapped rusqlite error with the DB path in
   the message) would leak that path to any browser viewer. SvelteKit
   auto-escapes the interpolation so raw XSS is blocked, but the info-
   disclosure is real. Now we strip `/path/to/file.{sqlite,rs,db,toml,
   lock}` patterns and cap the rendered string at 200 chars before it
   hits the DOM. The regex used to gate the empty-state branch still
   runs against the raw message so detection accuracy isn't affected.

2. Correctness nit (audit PATH D): `execute_check` in
   `intention_unified.rs` was dropping `intention.status` and
   `intention.snoozed_until` from the response JSON. When
   `include_snoozed=true` surfaces both active and snoozed intentions
   in the same list, callers cannot distinguish an active-triggered
   intention from a snoozed-overdue one. Expose both fields so the
   consumer (dashboard, CLI, Claude Code) can render them
   appropriately.

Neither change affects the default code path under
`include_snoozed=false`; regression risk is zero.
This commit is contained in:
Sam Valladares 2026-04-19 17:02:36 -05:00
parent 2da0a9a5c5
commit 83902b46dd
2 changed files with 111 additions and 3 deletions

View file

@ -93,13 +93,21 @@
// real errors (network down, dashboard disabled, 500s) and looked
// identical to a first-run install. Split the two so debugging
// isn't a guessing game.
const msg = e instanceof Error ? e.message : String(e);
//
// Sanitize the error string before rendering: strip filesystem
// paths and crate-file references (the backend occasionally wraps
// raw rusqlite / fs errors) and cap length at 200 chars so a
// stack-trace-sized error doesn't dominate the page.
const rawMsg = e instanceof Error ? e.message : String(e);
const safeMsg = rawMsg
.replace(/\/[\w./-]+\.(sqlite|rs|db|toml|lock)\b/g, '[path]')
.slice(0, 200);
const isEmpty =
(graphData?.nodeCount ?? 0) === 0 &&
/not found|404|empty|no memor/i.test(msg);
/not found|404|empty|no memor/i.test(rawMsg);
error = isEmpty
? 'No memories yet. Start using Vestige to populate your graph.'
: `Failed to load graph: ${msg}`;
: `Failed to load graph: ${safeMsg}`;
} finally {
loading = false;
}