Skip to content

Security & correctness audit remediation (+ Perry-runtime compat) - #2

Merged
proggeramlug merged 5 commits into
mainfrom
fix/perry-runtime-compat
Jul 4, 2026
Merged

Security & correctness audit remediation (+ Perry-runtime compat)#2
proggeramlug merged 5 commits into
mainfrom
fix/perry-runtime-compat

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 4, 2026

Copy link
Copy Markdown
Member

Full audit of the CMS followed by remediation across auth, authz, injection/XSS/SSRF, caching, the job queue, schema evolution, CI/build, and repo hygiene. The audit report is in fable-audit.md.

Highlights

Security

  • TOTP fails closed on both login paths (was: any 6-digit code accepted); session cookie Secure via X-Forwarded-Proto + HSTS/nosniff/frame headers; session tokens stored sha256-hashed at rest (migration 0002 clears old rows); login timing equalized; sessions invalidated on password change.
  • Role/user management allowlists capabilities and blocks built-in-role edits and assigning a role more privileged than the actor — manageRoles/manageUsers no longer escalate to admin.
  • Content status no longer mass-assignable (publish gate); admin content & form-submission views enforce read/readDrafts/viewSubmissions; GET /api/v1/settings filters sensitive keys.
  • @skelpo/site-kit renderMarkdown/renderTipTap sanitize raw HTML and block javascript:/data: URLs; webhook SSRF guard (+ opt-in WEBHOOK_ALLOW_PRIVATE_HOSTS) and fetch timeout; media upload size cap + nosniff/attachment on /raw; email HTML escaping; redirect-destination validation.

Correctness

  • Cache dep-key invalidation, TTL, eviction cleanup, compute-vs-invalidate guard; cursor pagination no longer loops.
  • Job handler timeout, recurring maintenance, scheduledPublish producer, unhandled-rejection guards.
  • Schema evolution: reject ambiguous rename diffs, migrate-before-validate, advance schemaRevision, optimistic locking on content updates; quote/comment-aware migration splitter + advisory migration lock.
  • Replaced instanceof Response with a shared isResponse() guard across every API route (Perry-safe; closes a latent fail-open).

Build / CI

  • tsconfig → NodeNext; hono 4.12.21 → 4.12.27 (clears CVEs); dropped unused @hono/node-server; ignore uploads-*/.
  • test.yml compiles dist/, boot-smokes /healthz, and fails when integration tests don't actually run; fixed the test:integration exit-code masking. release.yml installs from the workspace root + a pre-publish test gate.

⚠️ Operational notes (breaking)

  • Migration 0002 clears the sessions table — everyone re-logs in once after deploy (old plaintext tokens can't match a hashed lookup).
  • Schema renames via the API now require an explicit changes object (422 otherwise) — the guard against silent data loss.

Tests

77 unit + 34 integration, all green locally.

Follow-ups (not in this PR)

Pin GitHub Actions to commit SHAs; remaining DB-dependent integration tests for bearer auth / job concurrency; doc corrections listed in the audit.

Summary by CodeRabbit

  • New Features

    • Added scheduled content publishing and improved media handling, including clearer upload limits and safer file delivery behavior.
    • Expanded webhook configuration with clearer defaults for internal host access.
  • Security

    • Strengthened protection against unsafe links, redirects, webhooks, and email content to reduce XSS/SSRF risks.
    • Improved login, session, and role-management safeguards.
  • Bug Fixes

    • Fixed cache, publishing, pagination, and startup reliability issues for smoother admin and content workflows.

Perry's bcrypt is ~8s of CPU at cost-12 even after the typed-array perf fixes
(PerryTS/perry #5544/#5551; remaining gap tracked in #5525) vs ~250ms on Node —
too slow for a login request. cost-10 is ~2-3s, fine for a single admin login;
raise back once perry's bcrypt nears native speed. NEW hashes only — the seeded
admin password must be re-hashed at the new cost.

(Earlier this branch also carried an async→sync bcrypt swap and a parseCapabilities
Buffer guard; both proved unnecessary once PerryTS/perry #5485/#5521/#5563 landed —
async bcrypt works and the @perryts/mysql JSON column already decodes to an object.)

Claude-Session: https://claude.ai/code/session_012nEhNZCXKSeNStMnVD5E9e
Full audit of the CMS across auth, authz, injection/XSS/SSRF, caching,
the job queue, schema evolution, CI/build, and repo hygiene. Findings are
grouped by severity with file locations, impact, and fixes; the following
commits remediate them.
Auth/session:
- TOTP fails closed on both login paths (was: any 6-digit code accepted)
- session cookie Secure derived from X-Forwarded-Proto + HSTS/nosniff/
  frame-options/referrer-policy headers
- session tokens stored sha256-hashed at rest (migration 0002 clears the
  old plaintext rows); timing-equalized login; invalidate sessions on
  password change

Authz:
- role/user management allowlists capabilities and blocks built-in-role
  edits and assigning a role more privileged than the actor, so
  manageRoles/manageUsers no longer escalate to admin
- content status is no longer mass-assignable (publish gate); admin
  content and form-submission views enforce read/readDrafts/viewSubmissions;
  GET /api/v1/settings filters sensitive keys; readOthersDrafts enforced

Injection/XSS/SSRF:
- @skelpo/site-kit renderMarkdown/renderTipTap sanitize raw HTML and block
  javascript:/data: URLs
- webhook SSRF guard (private/loopback/link-local + metadata) and fetch
  timeout; media upload size cap + nosniff/attachment on /raw; email HTML
  escaping; redirect-destination validation

Correctness:
- cache dep-key invalidation, TTL, eviction cleanup, compute-vs-invalidate
  guard; cursor pagination no longer loops (MyDateTime serialization)
- job handler timeout, recurring maintenance, scheduledPublish producer,
  unhandled-rejection guards; boot-time non-UTC-DB warning
- schema evolution: reject ambiguous rename diffs, migrate-before-validate,
  advance schemaRevision, optimistic locking on content updates
- quote/comment-aware migration splitter + advisory migration lock

Portability:
- replace instanceof Response with a shared isResponse() guard across every
  API route (Perry-safe; closes a latent fail-open)

Adds unit tests for content XSS, privilege escalation, and the SQL splitter.
- tsconfig -> NodeNext (root + both packages): a missing .js import
  extension is now a compile error instead of a runtime break
- bump hono 4.12.21 -> 4.12.27 (clears GHSA-88fw-hqm2-52qc and related);
  drop unused @hono/node-server dependency; ignore uploads-*/ so a
  customer's local upload dir can't be committed
- test.yml: compile dist + boot-smoke /healthz + integration tripwire
  (fails when 0 tests run or all are skipped); fix the test:integration
  exit-code masking so a failing suite actually fails CI
- release.yml: install from the workspace root + a pre-publish unit-test
  gate; remove the stale per-package lockfiles
- carry forward in-progress Perry-runtime working changes (CLAUDE.md,
  seed.ts, build-perry.sh, media-pipeline.md)
…egration suite

- add WEBHOOK_ALLOW_PRIVATE_HOSTS (default off) so deployments/tests that
  intentionally deliver to an internal host can opt out of the SSRF guard;
  enable it in the integration test env (tests hit a local 127.0.0.1 sink)
- fix a stale integration test that POSTed to a bulk /admin/settings route
  that no longer exists — settings are edited per-key at
  /admin/settings/:keyName; the old CI masked this failure
- align the admin per-key settings save with the settings:all cache
  umbrella (was still using the old no-op invalidate)

Full suite now green locally: 77 unit + 34 integration.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dd896287-18d2-4216-a487-42dcd94618d3

📥 Commits

Reviewing files that changed from the base of the PR and between 58b9ae1 and f78f846.

⛔ Files ignored due to path filters (3)
  • package-lock.json is excluded by !**/package-lock.json
  • packages/cms-client/package-lock.json is excluded by !**/package-lock.json
  • packages/site-kit/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (55)
  • .env.example
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • .gitignore
  • CLAUDE.md
  • docs/media-pipeline.md
  • fable-audit.md
  • migrations/0002_hash_session_tokens.sql
  • package.json
  • packages/cms-client/tsconfig.json
  • packages/site-kit/src/markdown.ts
  • packages/site-kit/src/richtext.ts
  • packages/site-kit/src/url.ts
  • packages/site-kit/tsconfig.json
  • scripts/build-perry.sh
  • src/admin/routes.tsx
  • src/admin/screens.tsx
  • src/app.ts
  • src/auth/middleware.ts
  • src/auth/password.ts
  • src/auth/sessions.ts
  • src/cache/deps.ts
  • src/cache/lru.ts
  • src/cache/respond.ts
  • src/content/content.ts
  • src/content/schemaEvolution.ts
  • src/content/typeWriter.ts
  • src/content/writer.ts
  • src/db/client.ts
  • src/db/migrate.ts
  • src/db/seed.ts
  • src/email/adapter.ts
  • src/jobs/worker.ts
  • src/permissions/check.ts
  • src/routes/api/_helpers.ts
  • src/routes/api/auth.ts
  • src/routes/api/content.ts
  • src/routes/api/forms.ts
  • src/routes/api/jobs.ts
  • src/routes/api/media.ts
  • src/routes/api/menus.ts
  • src/routes/api/redirects.ts
  • src/routes/api/settings.ts
  • src/routes/api/types.ts
  • src/routes/api/users.ts
  • src/routes/api/webhooks.ts
  • src/server.ts
  • src/settings/store.ts
  • src/webhooks/dispatch.ts
  • tests/helpers/db.ts
  • tests/integration/admin.test.ts
  • tests/unit/migrate-split.test.ts
  • tests/unit/permissions-escalation.test.ts
  • tests/unit/site-kit-xss.test.ts
  • tsconfig.json

📝 Walkthrough

Walkthrough

This PR hardens authentication, session, and authorization controls (timing-safe login, hashed sessions, fail-closed TOTP, privilege-escalation guards, draft/publish authorization), adds XSS and SSRF protections, fixes cache/content/migration correctness bugs, reworks the server for Perry-native compilation, updates build/CI tooling, and adds two new documentation files.

Changes

Security Hardening

Layer / File(s) Summary
Auth guard, timing, and session hashing
src/auth/middleware.ts, src/auth/password.ts, src/auth/sessions.ts, migrations/0002_hash_session_tokens.sql, src/app.ts, src/routes/api/_helpers.ts
Adds isResponse type guard, verifyDummy() timing equalization, lowers bcrypt cost, hashes session ids via SHA-256, clears plaintext sessions via migration, adds security response headers and isHttps.
Login/session route hardening
src/admin/routes.tsx, src/routes/api/auth.ts
Reworks login flow with timing equalization for unknown users, fail-closed TOTP, and isHttps-aware cookies.
Auth guard rollout
src/admin/screens.tsx, src/routes/api/jobs.ts, src/routes/api/webhooks.ts, src/routes/api/types.ts
Replaces instanceof Response checks with notAuth/isResponse guards across routes.
Content draft/publish authorization
src/admin/routes.tsx, src/routes/api/content.ts
Adds ownership-based draft access and a publish capability gate.
Privilege-escalation guards
src/permissions/check.ts, src/routes/api/users.ts, tests/unit/permissions-escalation.test.ts
Adds isCapabilitiesShape/grantsWithinActor checks for role/user management.
XSS sanitization
packages/site-kit/src/markdown.ts, richtext.ts, url.ts, src/email/adapter.ts, src/routes/api/forms.ts, tests/unit/site-kit-xss.test.ts
Adds safeHref/safeSrc/escapeHtml and applies them in rendering and templating.
Webhook SSRF protection
src/webhooks/dispatch.ts, .env.example, tests/helpers/db.ts
Adds assertSafeWebhookUrl validation and a WEBHOOK_ALLOW_PRIVATE_HOSTS opt-in.
Media/redirect/settings safety
src/routes/api/media.ts, src/admin/screens.tsx, src/routes/api/redirects.ts, src/routes/api/settings.ts, src/settings/store.ts, tests/integration/admin.test.ts
Adds upload limits, safe Content-Disposition, redirect destination validation, and sensitive-setting filtering.

Estimated code review effort: 5 (Critical) | ~120 minutes

Caching and Content/DB Correctness

Layer / File(s) Summary
Cache generation/eviction tracking
src/cache/deps.ts, src/cache/lru.ts, src/cache/respond.ts
Adds a generation counter and eviction callback to prevent stale cache writes.
Menus cache invalidation
src/routes/api/menus.ts
Switches to an umbrella menus:all invalidation key.
Content pagination and publish concurrency
src/content/content.ts, src/content/writer.ts, src/content/schemaEvolution.ts, src/content/typeWriter.ts
Fixes cursor decoding/encoding, adds optimistic concurrency and field migration on publish.
Migration locking, timezone check, worker maintenance
src/db/migrate.ts, src/db/client.ts, src/db/seed.ts, src/jobs/worker.ts, tests/unit/migrate-split.test.ts
Adds advisory-lock migration runner, timezone check, and scheduled maintenance/publish loops.

Perry-Native Runtime and Build Tooling

Layer / File(s) Summary
Node HTTP adapter
src/server.ts
Replaces Bun/@hono/node-server with a node:http-to-fetch bridge and warmup call.
Perry build config
package.json, scripts/build-perry.sh, tsconfig.json, packages/*/tsconfig.json
Adds perry.compilePackages, a compile wrapper script, and NodeNext module settings.
CI/tooling docs
.github/workflows/*, .gitignore, CLAUDE.md
Updates release/test workflows and Perry status notes.

Documentation

Layer / File(s) Summary
Media pipeline design doc
docs/media-pipeline.md
Adds a proposal for content-addressed responsive media derivatives.
Security audit report
fable-audit.md
Adds a findings and remediation roadmap document.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthRoute
  participant Password
  participant Sessions
  Client->>AuthRoute: POST /login (email, password)
  alt user not found
    AuthRoute->>Password: verifyDummy()
  else user found
    AuthRoute->>Password: verifyPassword(hash)
  end
  AuthRoute->>Sessions: createSession(token)
  Sessions->>Sessions: sha256Hex(token) stored as id
  Sessions-->>AuthRoute: raw token (for cookie)
Loading
sequenceDiagram
  participant AdminAPI
  participant Dispatch
  participant Fetch
  AdminAPI->>Dispatch: createWebhook(url)
  Dispatch->>Dispatch: assertSafeWebhookUrl(url)
  AdminAPI->>Dispatch: deliverWebhookJob(hook)
  Dispatch->>Dispatch: assertSafeWebhookUrl(hook.url)
  Dispatch->>Fetch: fetch(url, AbortSignal.timeout)
  Fetch-->>Dispatch: response or timeout
Loading

Related Issues: None referenced in the diff.

Related PRs: None referenced in the diff.

Suggested labels: security, breaking-change, needs-deep-review

Suggested reviewers: Reviewers familiar with auth/session internals, cache invalidation, and Perry runtime constraints should review this PR given its scope and security-critical nature.

Poem

A rabbit hopped through hashed session lanes,
Dodged SSRF traps and javascript: chains,
Bcrypt slowed down, then sped up with care,
Webhooks now check before they dare,
Perry compiles, the server anew—
A warren of fixes, reviewed by few! 🐇

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/perry-runtime-compat

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit 26b70e9 into main Jul 4, 2026
2 of 3 checks passed
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.

1 participant