fix(security): audit follow-ups — response headers, import SVG sanitization, MCP token expiry#115
Merged
Merged
Conversation
…sniff, upload CSP)
…ay TTL) Adds token expiry to MCP connectors so bearer tokens no longer live forever. Existing live tokens receive a 90-day grace via a non-destructive backfill migration; new tokens always carry a computed expires_at from creation time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…disposable-DB guidance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alidation module
Root causes of the 3 failing tests:
1. `skips unselected media entries` and `full-site round-trip` (import-export-
roundtrip.test.ts): both fixtures used fake ASCII bytes as PNG content
(`strToU8('keep')`, `Buffer.from('fake-png-bytes')`). The new
`validateAndSanitizeMediaBytesForImport` gate introduced on this branch
calls `detectAcceptedMime()` on every staged entry before writing to disk;
plain ASCII returns null → ArchiveMediaValidationError → 400.
Fix: update both fixtures to use real PNG magic bytes (8-byte signature ±
padding to keep `sizeBytes` consistent with DB records).
2. `Module size budgets`: `importArchive.ts` grew to 771 lines (ceiling 700)
after the new validation class and function were added inline.
Fix: extract `ArchiveMediaValidationError` and
`validateAndSanitizeMediaBytesForImport` into the focused new module
`server/handlers/cms/importArchiveMediaValidation.ts` (97 lines);
`importArchive.ts` drops to 688 lines. Add the new helper to the
cms-handlers-capability-gated ALLOWLIST (it's not a route handler,
same pattern as mediaUpload.ts / svgSanitize.ts).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration 019 now only adds the expires_at column (no backfill UPDATE). Pre-existing / grandfathered rows have NULL expires_at, which the SQL predicate `(expires_at is null or expires_at > now)` already accepts as valid — no extra code needed. rowToRecord passes NULL through as-is (string | null) instead of coercing it to epoch-0. McpConnectorViewSchema and McpConnectorRecord updated to string | null. New tests assert that a connector with NULL expires_at is accepted by both findConnectorByTokenHash and resolveMcpAuth. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add an "Expires after" Select field (30 days / 90 days / 1 year, default 90) to the Add MCP Connector dialog. The selected value is converted to an integer and sent as `ttlDays` in the create request. The connector list now also renders `expiresAt` — formatted as a local date when set, or "No expiry" when null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s:null Add `null` as an explicit opt-in value for `ttlDays` when creating an MCP connector, meaning the token never expires. A number still produces a concrete expiry; omitting the field defaults to 90 days. The `expires_at` column is already nullable and `findConnectorByTokenHash` already treats NULL as non-expiring, so this is a pure schema/logic change with no migration required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a 'No expiry' choice to the TTL selector in the Add Connector dialog. Selecting it sends `ttlDays: null` to the backend, which the server now accepts as an explicit opt-in for a non-expiring token. Default selection remains 90 days. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DavidBabinec
marked this pull request as ready for review
June 30, 2026 23:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Security response headers
New
server/securityHeaders.tsapplied centrally inserver/index.ts:X-Content-Type-Options: nosniffandReferrer-Policyon all responses.Strict-Transport-Security— emitted only when the public origin is HTTPS (no HSTS over plain HTTP).X-Frame-Options: DENY+ CSPframe-ancestors 'none'on/admin/*— clickjacking protection for one-click publish/delete actions.default-src 'none'CSP on/uploads/*— prevents uploaded static files from being weaponised as script execution contexts.A full admin Content-Security-Policy was intentionally deferred (requires careful scoping for the
blob:canvas iframe + plugin bundles) — noted as a follow-up in code.Bundle-import hardening
importStagedArchiveMediaEntriesnow re-validates media via magic-byte MIME detection, rejects MIME/extension mismatches, and sanitizes SVG bytes before writing to disk — closing a gap where adata.importholder could smuggle an unsanitized/script-bearing SVG or a MIME-mismatched file past the upload pipeline's protections.server/handlers/cms/importArchiveMediaValidation.ts.MCP connector token expiry
Additive migration
019_mcp_connector_token_expiryadds a nullableexpires_atcolumn toai_mcp_connectors(both PG + SQLite dialects) and backfills existing rows.ttlDays, range 1–3650).findConnectorByTokenHashnow rejects expired tokens.expiresAtis surfaced in the connector view (the token hash is still never leaked).Docs
CLAUDE.mdcorrected: the database is not disposable — live, self-hosted installations exist with real user data. Schema changes ship as additive, non-destructive migrations (next sequential ID in both dialect files); committed migrations must never be rewritten or require a DB drop. The code-level no-backward-compat stance is retained with an explicit DB exception.Why
Follow-up remediation from a deep security audit of the CMS. These are the three HIGH-priority items from that audit.
Migration / upgrade impact
Migration
019is additive (ADD COLUMN expires_at+ backfillUPDATE) and runs automatically on pull without dropping data.Verification
Out of scope / follow-ups
The remaining MEDIUM audit findings are intentionally deferred:
blob:canvas + plugin-bundle scoping)These are tracked as follow-up issues.