Skip to content

security: persisted-state RCE/CSP hardening + register body cap (spec 09 §B/§C/§D) - #99

Merged
lopugit merged 76 commits into
developfrom
claude/eval-csp-hardening
Aug 18, 2026
Merged

security: persisted-state RCE/CSP hardening + register body cap (spec 09 §B/§C/§D)#99
lopugit merged 76 commits into
developfrom
claude/eval-csp-hardening

Conversation

@lopugit

@lopugit lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

PR #99 is now reconciled against current develop (6a026ecd, including merged PR #90). It keeps the security intent while using today’s persistence, autosave, Feed, /things, auth, and default-merge architecture.

Persisted Thingtime state is data, never a code-delivery mechanism:

  • Functions are omitted on write, including Functions with custom toJSON().
  • Every legacy { ttype: "function", ... } value is removed graph-safely without compiling or executing its source.
  • Code-defined defaults restore legitimate runtime functions.
  • Circular references and shared aliases remain intact.
  • Real Dates use explicit { ttype: "date", iso } tags.
  • Every untagged string—including exact ISO timestamps—stays a string. Ambiguous legacy ISO values are preserved as text rather than guessed into Dates.

CSP and dynamic execution

  • Production app routes use a shared CSP with neither executable inline scripts nor unsafe-eval.
  • Theme/pre-paint and environment-title boot run from same-origin /tt-boot.js.
  • Preview freshness runs from external /tt-preview-freshness.js before the application entry.
  • Current and deprecated Commander assignment paths parse JSON-compatible data, quoted strings, or plain text; program text is never evaluated.
  • Legacy opt-in smarts compiler/global-lookup branches remain intentionally deferred and blocked by the application CSP.
  • Repository-controlled Design bundles alone retain a path-scoped compiler/unpkg policy inside an opaque-origin sandbox.

Future executable Thingtime behavior belongs in an explicit Action/Command/Function registry or isolated runtime; this PR does not design that system or weaken the app-wide boundary.

Registration

PR #167 remains the single canonical auth.register limiter: 10 attempts per 15 minutes per IP, admin-tunable, enforced before parsing. PR #99 does not duplicate or change it.

Registration now uses shared readJsonBody(request, 16 * 1024), returning the standard 413 before validation, bcrypt, or account writes. The API regression uses a unique RFC 3849 test IP so a consumed limiter bucket cannot mask the cap.

Reconciliation decisions

  • Folded the invariants into active app/Providers/thingtimeSerialization.ts.
  • Removed the obsolete parallel thingtimePersistCodec implementation.
  • Preserved current autosave, atomic repair, mutation queue, temporary-user, Feed, /things, auth, and default-merge behavior.
  • Removed unrelated rebase-stack resolver changes accumulated through historical conflict merges.
  • Left cross-tab PR Cross-tab sync for persisted thingtime state via BroadcastChannel (claude-todo/07) #92 separate; it should use the active safe serializer when rebased.

Detailed note: PRs/99-claude-eval-csp-hardening--persisted-state-csp-register-body-cap.md

Validation

Local final-head results:

  • test:persist: 11/11
  • test:autosave: 23/23
  • test:commander: 5/5
  • test:preview-build: 7/7
  • test:rate-limit: 1/1
  • Full test:unit: passed
  • Full headless API suite: 423/423
  • Production Vite + Nitro/Vercel build and CSP/output verifier: passed
  • typecheck:ratchet: non-blocking 147 vs baseline 143, one fewer than isolated current develop (148)
  • Touched-file ESLint: zero errors; repository-wide lint reaches six pre-existing import/first errors in unchanged kindRegistry.tsx
  • git diff --check origin/develop: passed
  • Graphify semantic graph/report/HTML refreshed; generated output committed separately

Rendered desktop and 390×844 mobile QA covered /, /things, Feed, login, register, Commander, nested controls, top-to-bottom layout, theme boot, hostile Commander text, and two persistence reload cycles. No CSP/runtime errors occurred; only the existing React Router HydrateFallback warning was present.

Published head: dd3efc9faab0e0ccf21493e469dbc38beb4145c2.

Final publication state:

  • GitHub reports MERGEABLE / CLEAN against develop 6a026ecd58800dbdd72ea5b3e5d9cb2d3d928ce4; there are no review threads.
  • Web CI build/typecheck/unit and the full API job passed.
  • Vercel and Vercel Deployments passed. Branch preview · exact-SHA develop preview · deployment.
  • GitGuardian is neutral because its scan exceeded the allocated time, with zero annotations; this is an incomplete scan, not a finding. An independent redacted Gitleaks 8.30.1 scan covered all 38 PR commits and 286 MB. Its 20 matches were all generated-Graphify false positives on three symbol-list strings; no credential value was identified.

…rruption

TODO/claude-todo/09-security-hardening.md sections C and D.

Persisted-state eval (RCE): ThingtimeProvider revived any persisted
{ttype:'function'} value via eval(code), and the replacer serialized every
function that way into localforage/IndexedDB on each state change. Anything able
to write same-origin storage (an XSS, an extension, another tab) could plant a
payload that executed on every subsequent load. Functions are now dropped on
both write and read; ThingtimeDefaults re-supplies the real ones on hydrate.

CSP: no Content-Security-Policy existed anywhere, so 'unsafe-eval' was
effectively allowed. Added a single-source policy (scripts/csp.mjs) with NO
'unsafe-eval', stamped as a header on the Vercel output (patch-vercel-output)
and the Vite dev server, and asserted by verify-vercel-output. The two inline
boot scripts in index.html moved to an external /tt-boot.js so the policy can
stay script-src 'self' without per-edit inline hashes.

Date.parse corruption: the reviver turned ANY string that passed the lenient
Date.parse ("Post 1", "2024", "March 2024") into a Date on reload, then the
replacer rewrote it as an ISO string, permanently corrupting user data after
one save/reload cycle. Revival is now restricted to strict ISO-8601 timestamps.

The codec moved to app/Providers/thingtimePersistCodec.ts (pure, React-free)
with regression tests in thingtimePersistCodec.test.ts (npm run test:persist,
6 cases: strings survive round-trips, real Dates revive, functions/hostile
payloads are dropped).

Verified: full build + verify:vercel-output pass; test:persist 6/6; app
hydrates and renders correctly under the CSP in a live browser with fonts and
Emotion styles loading unblocked. Pre-existing hydrateRoot(document) DOM errors
(TODO #1) are unchanged and reproduce identically without this change.

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 Aug 17, 2026 10:54pm
thingtime (develop) Ready Ready Preview Aug 17, 2026 10:54pm

Request Review

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lopugit and others added 2 commits July 21, 2026 14:14
TODO/claude-todo/09-security-hardening.md §B. Login and resend-verification
already enforce the shared config-driven limiter; register was the last auth
endpoint with neither a rate limit nor a body cap. Each signup creates a user,
burns a bcrypt hash, and queues a verification email/outbox row, so an
unbounded register is a resource-amplification + mailbox-spam vector.

- Add an `auth.register` rule to RATE_LIMIT_DEFAULTS (10 / 10 min per IP,
  admin-tunable; flows through RATE_LIMIT_ENDPOINTS + the admin panel
  automatically).
- Enforce it in the register action before any work (fail-open, like the other
  auth routes) and switch the raw `request.json()` to `readJsonBody(request,
  16 KB)` so an oversized payload is rejected 413 before it is buffered.

The `meta` mass-assignment concern in §B is already handled — the route
whitelists username/password/email/displayName and never passes `meta`.

Tests (app/tests/api/apiTests.ts): auth-register-validation now also accepts a
shape-checked 429; new auth-register-body-cap asserts an oversized body is
rejected 413.

Verified live against local Nitro: a fresh per-IP bucket returns 10x 400 then
429, and an oversized body returns 413. Full build + verify:vercel-output pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lopugit lopugit changed the title security: kill persisted-state eval RCE + add CSP; stop Date.parse corruption security: auth register rate-limit + persisted-state eval RCE/CSP + Date.parse corruption (spec 09 §B/§C/§D) Jul 21, 2026
@lopugit

lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Triage note (session 3/10): this overlaps #94 (TODO 9, date reviver) and #102 (TODO 10, eval removal + CSP — stacked on #94 since both rewrite the same reviver/replacer). #102's CSP was verified against the built shell (hash-allowed inline scripts, injected-script block, eval intact for Commander/smarts). Owner should pick one lineage.

lopugit and others added 2 commits July 29, 2026 12:33
…15 follow-up)

useThingtime.tsx carried its own copy of ThingtimeTypes/EverythingTypes,
drifted from the canonical definitions in ThingtimeProvider.tsx (an
unused optional Provider field). The hook now imports and re-exports the
provider's types — one source of truth, no consumer changes (the only
external importer, ThingtimeURL.tsx, already imports from the provider;
nothing reads the dropped Provider field).

The remaining item-15 Commander V1→V2 swap is deliberately sequenced
AFTER PR #130 lands: CommanderV2 lacks the pathPrefix/placeholder props
the Thingtime.tsx inline call site depends on, and #130 is actively
rewriting CommanderV2 — porting those props now would guarantee
conflicts.

Verified: tsc --noEmit reports no errors in the touched files; /feed and
/things render live with zero console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…98/#106)

Upgrade the persist codec from strict-ISO string revival to the tagged
scheme the duplicate TODO-9 PRs used, closing the remaining false-positive:

- Real Dates persist as {ttype:'date', iso}. The replacer reads the
  original off the holder (this[key]) because Date.toJSON converts Dates
  to strings before the replacer sees the value.
- A USER string that merely looks like a full ISO timestamp is escaped as
  {ttype:'iso-string', s} so the legacy fallback can never capture it —
  previously such a string became a Date on the next load.
- The legacy bare-ISO fallback remains only to migrate pre-tagging
  persists.
- Both replacer and reviver skip their own wrappers' inner keys (without
  the guard the escape rule re-wraps its own output unboundedly, and the
  legacy fallback hands the wrapper branches a Date instead of the
  original string — caught by the new tests).

Tests: 9/9 in test:persist, including Date + identical-looking string
coexisting, two-cycle string stability, and hostile-payload drops.
TESTING.md gains the register rate-limit checklist (from #106) and a
persisted-codec checklist. Live-verified: app hydrates under the CSP,
persists and reloads cleanly, zero console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lopugit

lopugit commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Consolidation complete and live-verified on the current #99 branch.

Current proof: test:persist 10/10, provider/autosave 22/22, focused lint, full Vite/Nitro/Vercel build, strengthened output verifier, desktop/mobile Browser QA, safe Commander interactions, sandboxed design-bundle render, and a live Vercel inline-script probe that attached but did not execute under CSP.

Conflicted paths: TESTING.md, remix/app/api/utils/rateLimit/config.ts

Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/30546808318

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤝 Merged main into claude/eval-csp-hardening — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • TESTING.md
  • remix/app/api/utils/rateLimit/config.ts

Please review the merge commit before relying on it.

Conflicted paths: TESTING.md

Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/30547496533

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤝 Merged main into claude/eval-csp-hardening — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • TESTING.md

Please review the merge commit before relying on it.

@github-actions

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/eval-csp-hardening — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • graphify-out/GRAPH_REPORT.md
  • graphify-out/cache/semantic/0000c1861bd35c3bb0fc103f4888f4a7d4b1a29e6f32ce90738afdb98628214f.json
  • graphify-out/cache/semantic/051c543ef636903da05b8583c1046c61069b951160ed5f11ad1852c188e52464.json
  • graphify-out/cache/semantic/3959b61faacc84665104960d9a0e095d764f17030bbd31232e1661848e0d5de7.json
  • graphify-out/cache/semantic/421b9825c007651de1cf5776993e5c72f348275707045a074b6733ee35a1266b.json
  • graphify-out/cache/semantic/4b6debe4f061cf64298ddda55336a1179f2ef60f4a4687ad4404e46766b8ff4c.json
  • graphify-out/cache/semantic/4c2c4ec4f162a34ffa9498080eba43546a2d4d8d2b84e16278189372263dcae0.json
  • graphify-out/cache/semantic/6a5448ca1dd0c12bd3d1e269a805336dc8cefeeb825ebba49b7056da8ae4c0fb.json
  • graphify-out/cache/semantic/c560b533fee3f177db20b19ebb0fe64b1dee6d38e9fafbdeb01e5362e447f8bd.json
  • graphify-out/cache/semantic/ec7fec89546443193d9bbb21124e3851b8735ae1200eb4cdcbc7244344597a0c.json
  • graphify-out/cache/semantic/f2dfa6cdeab701d9dc85ae32c2a1f6f2f0d54f36505f53f81c78f3d8ab8c4dba.json
  • graphify-out/cache/semantic/ff517410932e72c2a5b4a2222835c2d7c7fe3e325ede82afe28c6018f260d3cd.json
  • graphify-out/graph.json
  • graphify-out/manifest.json
  • remix/app/Providers/ThingtimeProvider.tsx
  • remix/app/routes/api/v1/auth/register/_register.tsx
  • remix/scripts/verify-vercel-output.mjs
  • remix/vite.config.ts

graphify-out/ was reset wholesale to the develop side (repo rule: one side, never mixed — the graph merge driver is unavailable in CI).
Then re-ran graphify on the merged code WITH LLM semantic extraction (graphify extract, claude-cli backend) and committed the result — content new to this merge is semantically indexed; unchanged content came from the tracked cache.

Please review the merge commit before relying on it.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/eval-csp-hardening — conflicts auto-resolved by the resolve-pr-conflicts workflow.

No AI resolution was needed by merge time; the branch was updated with a plain merge commit.

graphify-out/ was reset wholesale to the develop side (repo rule: one side, never mixed — the graph merge driver is unavailable in CI).
Then re-ran graphify on the merged code WITH LLM semantic extraction (graphify extract, claude-cli backend) and committed the result — content new to this merge is semantically indexed; unchanged content came from the tracked cache.

Please review the merge commit before relying on it.

# Conflicts:
#	graphify-out/GRAPH_REPORT.md
#	graphify-out/cache/semantic/0000c1861bd35c3bb0fc103f4888f4a7d4b1a29e6f32ce90738afdb98628214f.json
#	graphify-out/cache/semantic/3959b61faacc84665104960d9a0e095d764f17030bbd31232e1661848e0d5de7.json
#	graphify-out/cache/semantic/7e3eb3c8b46208f0314d8cc4ec078cb0f7f8ebcf1f2df180b5b085215fbf064e.json
#	graphify-out/cache/semantic/f2dfa6cdeab701d9dc85ae32c2a1f6f2f0d54f36505f53f81c78f3d8ab8c4dba.json
#	graphify-out/graph.json
#	graphify-out/manifest.json
`graphify extract` with LLM semantic extraction (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache.

Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32000707437
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/eval-csp-hardening — conflicts auto-resolved by the resolve-pr-conflicts workflow.

No AI resolution was needed by merge time; the branch was updated with a plain merge commit.

graphify-out/ was reset wholesale to the develop side (repo rule: one side, never mixed — the graph merge driver is unavailable in CI).
Then re-ran graphify on the merged code WITH LLM semantic extraction (graphify extract, claude-cli backend) and committed the result — content new to this merge is semantically indexed; unchanged content came from the tracked cache.

Please review the merge commit before relying on it.

# Conflicts:
#	graphify-out/GRAPH_REPORT.md
#	graphify-out/cache/semantic/3959b61faacc84665104960d9a0e095d764f17030bbd31232e1661848e0d5de7.json
#	graphify-out/cache/semantic/579a3360eaf1876a4a317fb37b0e6d1dc3186ef98659ed134f921f5bba33cd80.json
#	graphify-out/cache/semantic/72e4dd8209d242d78ae8a9d62bc43a97251fe4a677454448cc4ed3bf632a0b4e.json
#	graphify-out/cache/semantic/8be894f31ddca5485296d5826b8b5e70aa13c85c16c8e711f7d5e47aa20539cd.json
#	graphify-out/graph.json
#	graphify-out/manifest.json
`graphify extract` with LLM semantic extraction (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache.

Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32003386552
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/eval-csp-hardening — conflicts auto-resolved by the resolve-pr-conflicts workflow.

No AI resolution was needed by merge time; the branch was updated with a plain merge commit.

graphify-out/ was reset wholesale to the develop side (repo rule: one side, never mixed — the graph merge driver is unavailable in CI).
Then re-ran graphify on the merged code WITH LLM semantic extraction (graphify extract, claude-cli backend) and committed the result — content new to this merge is semantically indexed; unchanged content came from the tracked cache.

Please review the merge commit before relying on it.

# Conflicts:
#	graphify-out/GRAPH_REPORT.md
#	graphify-out/cache/semantic/0000c1861bd35c3bb0fc103f4888f4a7d4b1a29e6f32ce90738afdb98628214f.json
#	graphify-out/cache/semantic/1bf59c49afae19114fb4d0993249bbc091b4454e8a790ec97586ecf346808e15.json
#	graphify-out/cache/semantic/3959b61faacc84665104960d9a0e095d764f17030bbd31232e1661848e0d5de7.json
#	graphify-out/cache/semantic/8e8e4267e034c0d47fe7a147325c47f46bcb58fb2a5483b6ab3f5e8efd69cd07.json
#	graphify-out/cache/semantic/f2dfa6cdeab701d9dc85ae32c2a1f6f2f0d54f36505f53f81c78f3d8ab8c4dba.json
#	graphify-out/graph.json
#	graphify-out/manifest.json
`graphify extract` with LLM semantic extraction (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache.

Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32008360215
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

⚠️ The resolver stopped in a later safety, verification, or publication step. No residual conflicted paths were confirmed; see the workflow run.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

⚠️ The resolver stopped in a later safety, verification, or publication step. No residual conflicted paths were confirmed; see the workflow run.

@github-actions github-actions Bot added the ai-rebase-in-progress AI stack rebase currently owns this PR branch label Aug 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ The AI PR/stack rebase workflow did not complete. The workflow stopped before it attempted its force-with-lease push.

ai-rebase-paused was added for this exact ref/topology snapshot; review the run, then use a manual PR-number run to retry.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ The AI PR/stack rebase workflow did not complete. The workflow stopped before it attempted its force-with-lease push.

The PR changed refs or resolver ownership, so no pause was added; the current state remains eligible for automatic re-detection.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/eval-csp-hardening — conflicts auto-resolved by the resolve-pr-conflicts workflow.

No AI resolution was needed by merge time; the branch was updated with a plain merge commit.

graphify-out/ was reset wholesale to the develop side (repo rule: one side, never mixed — the graph merge driver is unavailable in CI).
Then re-ran graphify on the merged code WITH LLM semantic extraction (graphify extract, claude-cli backend) and committed the result — content new to this merge is semantically indexed; unchanged content came from the tracked cache.

Please review the merge commit before relying on it.

github-actions Bot and others added 4 commits August 17, 2026 13:45
# Conflicts:
#	graphify-out/GRAPH_REPORT.md
#	graphify-out/cache/semantic/b72c0b5bfbc6a13338a54cbeb0197715411e8d71d4021d5e53e2d4a565d205bc.json
#	graphify-out/cache/semantic/c4d90b648c3b7e1d2f9f5b3fabbd8999abf03f2e3bce2989672675eae96f380f.json
#	graphify-out/graph.json
#	graphify-out/manifest.json
`graphify extract` with LLM semantic extraction (graphify 0.9.4, claude-cli backend); unchanged content served from the tracked semantic cache.

Refreshed by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32036302348
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🧹 Develop S3 preview removed

The PR-specific alias and every workflow-created develop deployment were removed when this PR closed.

The ordinary generated Vercel Preview remains available on the shared development runtime.

@lopugit

lopugit commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

🤖 Promotion conflict resolution was queued automatically for promote/pr-99-eval-csp-hardening--to-main at exact base main (fd970e9ab4bb729e2117f92e9afc8e9782154a61).

Conflicted source paths: remix/CHANGELOG.md.

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

✅ The trusted promotion worker opened #297 for promote/pr-99-eval-csp-hardening--to-main.

Review the exact resolved paths and immutable snapshot in the promotion PR comment. Workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Verified automatic promotion resolution for promote/pr-99-eval-csp-hardening--to-main at b134f744b019c2898d7a77f4f7bcd64c927de800.

This marker is inert until the live remote branch equals the attested head. Workflow run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant