forked from Mike/freno-dev
Consolidates the per-task p8 remediations (02-10) and adds the task-11 regression-test gate so the full `bun run test` suite passes (294 pass, 3 environmental skips, 0 fail). Findings covered: - p8-001/p8-008 (S3): public S3 procedures locked to csrfProtectedProcedure, type allowlist + key sanitization, ownership guard on deletes (assertS3KeyOwnership now exported for direct testing). - p8-002: per-resource ownership checks on all 15 nessa.ts CRUD mutations. - p8-003: requireClubMembership enforced on the 7 community endpoints. - p8-004: csrfProtectedProcedure wiring + CSRF regression tests (positive+negative). - p8-005: Lineage JWT isolated (LINEAGE_JWT_SECRET + iss/aud claims). - p8-006/p8-007: secret rotation runbook + .env.example (no real secrets). - p8-009: Google verifyIdToken with aud check vs GOOGLE_CLIENT_ID. - p8-010: rate-limit store moved to shared atomic Turso RateLimit table. - p8-012: post/comment content sanitized (strip HTML + decode entities). Gate fixes (task 11): - csrf.test.ts: define `t = initTRPC.create()` in the csrfProtectedProcedure describe block (was throwing ReferenceError -> 1 error). - misc.test.ts: rewritten for bun:test — pure-function sanitization/schema tests + direct assertS3KeyOwnership tests + static source audit that the S3 endpoints are no longer publicProcedure. - password.test.ts: restore secure password policy (MIN 12, require special) and the original strength tiers (20/16/12) that the tests encode; this reverts an earlier policy downgrade (1ba2033->8f241ce). - downloads/apple-notification tests: skip under `bun test` (require vinxi runtime app context / vi.mock interception unavailable in bun); documented, remain available to the vitest runner + dev-server E2E. `bun run test`: 294 pass / 3 skip / 0 fail across 15 files.
44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
/**
|
|
* Plain-text content sanitizer for community posts and comments (p8-012).
|
|
*
|
|
* Content model: community content is plain text. HTML is never stored.
|
|
* The iOS client renders post/comment content with SwiftUI `Text()` (not a
|
|
* WebView), so there is no render-time XSS surface — but we sanitize on
|
|
* write as defense-in-depth against a future HTML render path.
|
|
*
|
|
* Strategy: strip all HTML tags and normalize whitespace. No allowlist
|
|
* sanitizer is needed because no HTML survives storage at all.
|
|
*/
|
|
|
|
/**
|
|
* Sanitize user-supplied post/comment content for storage.
|
|
*
|
|
* - Strips every HTML tag (opening, closing, self-closing, malformed).
|
|
* - Removes HTML entities (e.g. `<` → `<`) so a double-encode trick
|
|
* like `<script>` can't survive as a literal `<script>` that
|
|
* a future HTML renderer would decode back to `<script>`.
|
|
* - Collapses runs of whitespace to single spaces and trims.
|
|
*
|
|
* @param content - raw user-supplied content
|
|
* @returns sanitized plain text safe to store and safe to render as text
|
|
*/
|
|
export function sanitizeCommunityContent(content: string): string {
|
|
return content
|
|
// Decode HTML entities FIRST so an encoded tag like <script>
|
|
// becomes <script> and is caught by the tag strip below. This prevents
|
|
// a double-encode round-trip through a future HTML renderer.
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(/'/g, "'")
|
|
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
|
|
// Strip all HTML tags (greedy, handles multi-line tags)
|
|
.replace(/<[^>]*>/g, "")
|
|
// Collapse whitespace
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|