security: persisted-state RCE/CSP hardening + register body cap (spec 09 §B/§C/§D) - #99
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
|
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. |
…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>
|
Consolidation complete and live-verified on the current #99 branch.
Current proof: |
# Conflicts: # TESTING.md
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>
|
🤝 Merged Conflicted files:
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>
|
🤝 Merged Conflicted files:
Please review the merge commit before relying on it. |
|
🤝 Merged Conflicted files:
Please review the merge commit before relying on it. |
|
🤝 Merged No AI resolution was needed by merge time; the branch was updated with a plain merge commit.
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
|
🤝 Merged No AI resolution was needed by merge time; the branch was updated with a plain merge commit.
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
|
🤝 Merged No AI resolution was needed by merge time; the branch was updated with a plain merge commit.
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
|
|
|
|
|
|
|
The PR changed refs or resolver ownership, so no pause was added; the current state remains eligible for automatic re-detection. |
|
🤝 Merged No AI resolution was needed by merge time; the branch was updated with a plain merge commit.
Please review the merge commit before relying on it. |
# 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
🧹 Develop S3 preview removedThe 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. |
|
🤖 Promotion conflict resolution was queued automatically for Conflicted source paths: The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed. |
|
✅ The trusted promotion worker opened #297 for Review the exact resolved paths and immutable snapshot in the promotion PR comment. Workflow run. |
|
🤖 Verified automatic promotion resolution for This marker is inert until the live remote branch equals the attested head. Workflow run. |
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:
toJSON().{ ttype: "function", ... }value is removed graph-safely without compiling or executing its source.{ ttype: "date", iso }tags.CSP and dynamic execution
unsafe-eval./tt-boot.js./tt-preview-freshness.jsbefore the application entry.smartscompiler/global-lookup branches remain intentionally deferred and blocked by the application CSP.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.registerlimiter: 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
app/Providers/thingtimeSerialization.ts.thingtimePersistCodecimplementation./things, auth, and default-merge behavior.Detailed note: PRs/99-claude-eval-csp-hardening--persisted-state-csp-register-body-cap.md
Validation
Local final-head results:
test:persist: 11/11test:autosave: 23/23test:commander: 5/5test:preview-build: 7/7test:rate-limit: 1/1test:unit: passedtypecheck:ratchet: non-blocking 147 vs baseline 143, one fewer than isolated currentdevelop(148)import/firsterrors in unchangedkindRegistry.tsxgit diff --check origin/develop: passedRendered 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 RouterHydrateFallbackwarning was present.Published head:
dd3efc9faab0e0ccf21493e469dbc38beb4145c2.Final publication state:
develop6a026ecd58800dbdd72ea5b3e5d9cb2d3d928ce4; there are no review threads.