Skip to content

fix(persist): stop Date-reviver corruption + kill persisted-state eval RCE, add CSP (TODO 9 + 10) - #94

Closed
lopugit wants to merge 5 commits into
mainfrom
claude/todo9-date-reviver-strict
Closed

fix(persist): stop Date-reviver corruption + kill persisted-state eval RCE, add CSP (TODO 9 + 10)#94
lopugit wants to merge 5 commits into
mainfrom
claude/todo9-date-reviver-strict

Conversation

@lopugit

@lopugit lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Fixes TODO 9 (persist Date-reviver data corruption) and TODO 10 (persisted-state eval RCE + CSP) — both rewrite the same ThingtimeProvider reviver/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" became Date objects 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 (like JSON.stringify) calls Date.toJSON before the replacer runs, so the replacer's old instanceof Date branch 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 CSP

The 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:

  • Persisted {ttype:'function'} values now revive as inert warn stubs (no eval); 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: 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 serves index.html. 'unsafe-eval' is retained deliberately — smarts + the Commander evaluate user-typed JS by design; dropping it is a separate migration. No frame-ancestors (embedding is a feature).

Verification

  • Node suites vs real 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.
  • Live browser (built shell via 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; eval still works for the Commander.

Updates TODO/TODO.md items 9 & 10 and claude-todo/09-security-hardening.md §C/§D.

Session 3 of 10 parallel todo sessions.

🤖 Generated with Claude Code

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>
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview, Comment Jul 29, 2026 2:31am

Request Review

@lopugit

lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Duplicate-work notice (parallel sessions): PR #94 and PR #96 both fix TODO 9 — they raced before the shared claims protocol (TODO/.claims/, created by session 1 mid-run) existed. One should be merged and the other closed. A precise comparison to help pick:

Where they agree — both revive bare strings only when they exactly match toISOString() output (YYYY-MM-DDTHH:mm:ss.sssZ) with a Date.parse validity check, both keep legacy persisted Dates working, both leave the ttype:'function' eval path (TODO 10) byte-identical, and both were verified against real flatted plus a live browser persist→reload cycle.

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 TODO/TODO.md ✅-annotation hunk. Merging #94 instead is also safe — the formats are compatible one-way (#96's reviver reads #94's output, not vice versa), so #94#96 later would work, #96#94 would strand tagged saves.

(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>
@lopugit lopugit changed the title fix(persist): stop reviving ordinary strings as Dates (TODO 9 data corruption) fix(persist): stop Date-reviver corruption + kill persisted-state eval RCE, add CSP (TODO 9 + 10) Jul 29, 2026
Comment thread remix/vite.config.ts
transformIndexHtml: {
order: 'post' as const,
handler(html: string) {
const inlineScriptHashes = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(
@lopugit

lopugit commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Consolidating the TODO 8/9/10 security PRs: #99 is the single winner and now carries this PR's unique value (see the consolidation note on #99 for exactly what was folded in and why). Closing as a duplicate.

@lopugit lopugit closed this Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants