fix(persist): stop Date-reviver corruption + kill persisted-state eval RCE, add CSP (TODO 9 + 10) - #94
fix(persist): stop Date-reviver corruption + kill persisted-state eval RCE, add CSP (TODO 9 + 10)#94lopugit wants to merge 5 commits into
Conversation
The localforage persist reviver revived ANY string passing V8's lenient Date.parse — "Post 1", "1", "2024", "March 2024" all became Date objects on reload and re-persisted as ISO strings, permanently corrupting user data after one save/reload cycle. Now only exact toISOString() output revives as a Date. That is the real persist format: flatted invokes Date.toJSON before any replacer runs, so the replacer's instanceof Date branch was dead code (removed). Legacy saves (bare ISO strings) still revive as Dates unchanged. Verified against real flatted (21-check node suite) and live in the browser through the app's own load->autosave cycle: probe strings survive re-persist untouched; a legacy ISO string revives as the tree's only Date; no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Duplicate-work notice (parallel sessions): PR #94 and PR #96 both fix TODO 9 — they raced before the shared claims protocol ( Where they agree — both revive bare strings only when they exactly match Where they differ — the persist format going forward:
Recommendation: merge #96 (tagged format is strictly more future-proof and matches the TODO's suggested scheme), and cherry-pick #94's nice (Posted by session 1; identical comment on both PRs.) |
…DO 10)
The localforage reviver eval'd {ttype:'function'} payloads on every load,
so anything able to write same-origin storage (XSS, extension, another
tab) gained persistent arbitrary code execution. Persisted functions now
revive as inert warn stubs and functions are dropped from persist
entirely (load falls back to code-defined defaults).
A build-only Vite plugin injects a CSP meta tag into the static shell
(meta so it applies wherever Vercel serves index.html, bypassing Nitro):
script-src 'self' + sha256 hashes for the shell's two inline scripts --
no 'unsafe-inline' for scripts, so injected inline/remote scripts are
blocked. 'unsafe-eval' is retained deliberately: smarts and the
Commander evaluate user-typed JS by design; dropping it is blocked on
migrating them off eval. No frame-ancestors -- embedding is a feature.
Verified: 26-check node suite (attack payload never executes at parse or
stub-call; functions dropped; TODO 9 date semantics unchanged) and live
against the built shell via vite preview -- app boots and renders under
the CSP, zero violations from the shell's own scripts (hash equality
confirmed in-browser), an injected inline script IS blocked, and eval
still works for the Commander.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| transformIndexHtml: { | ||
| order: 'post' as const, | ||
| handler(html: string) { | ||
| const inlineScriptHashes = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map( |
Fixes TODO 9 (persist Date-reviver data corruption) and TODO 10 (persisted-state
evalRCE + CSP) — both rewrite the sameThingtimeProviderreviver/replacer, so they're combined into one PR. (Consolidated from the former #102, whose commit is merged into this branch.)TODO 9 — 🐛 persist reviver turned ordinary strings into Dates
The localforage reviver revived any string passing V8's lenient
Date.parse:"Post 1","1","2024","March 2024"becameDateobjects on reload, then re-persisted as ISO strings — permanently corrupting user data after one save/reload cycle.Fix: revive only exact
toISOString()output. That's the real persist format — flatted (likeJSON.stringify) callsDate.toJSONbefore the replacer runs, so the replacer's oldinstanceof Datebranch was dead code and is removed. Legacy saves (bare ISO strings) still revive as Dates unchanged.TODO 10 — 🔒 persisted-state
eval= arbitrary code execution + no CSPThe same reviver ran
eval(value.code)for any{ttype:'function'}value on every load. Anything able to write same-origin storage (XSS, extension, another tab) gained persistent arbitrary code execution.Fix:
{ttype:'function'}values now revive as inert warn stubs (noeval); functions are dropped from persist entirely (load falls back to code-defined defaults).script-src 'self'+ sha256 hashes for the shell's two inline scripts (no'unsafe-inline'for scripts → injected inline/remote scripts blocked). Delivered as a meta tag so it applies wherever Vercel servesindex.html.'unsafe-eval'is retained deliberately — smarts + the Commander evaluate user-typed JS by design; dropping it is a separate migration. Noframe-ancestors(embedding is a feature).Verification
flatted(reviver/replacer extracted verbatim): everyday strings survive persist→reload; real Dates round-trip; legacy blobs still revive Dates; an attack payload never executes at parse or stub-call; functions dropped from persist.vite preview): probe strings survive the app's own load→autosave cycle with a legacy ISO date as the only revived Date; app boots under the CSP with zero violations from its own scripts (in-browser hash equality confirmed); an injected inline script is blocked;evalstill works for the Commander.Updates
TODO/TODO.mditems 9 & 10 andclaude-todo/09-security-hardening.md§C/§D.Session 3 of 10 parallel todo sessions.
🤖 Generated with Claude Code