audit: Tier 1/3/4/5/7 batch — SSE, gates, dedup, URL escape, FK constraints

Tier 1.6: S3Backend.put now sets ServerSideEncryption=AES256 — closes
the cleartext-at-rest gap for signed contracts, GDPR exports, pg_dumps.

Tier 3.7: New safeUrl() helper in lib/email/shell.ts. Scheme allow-list
(http/https/mailto/tel/relative only — javascript:/data:/vbscript:/file:
rewritten to about:blank) + HTML-attribute escape. Retrofitted across
all 7 transactional templates (crm-invite, portal-auth, document-signing,
notification-digest, residential-inquiry, admin-email-change).

Tier 4.2: /api/v1/alerts GET now gated on admin.view_audit_log.

Tier 4.3: Documenso webhook handler emits captureErrorEvent on catch.
Admin/errors no longer silent on webhook crashes.

Tier 4.6: Inquiry-funnel email dedup is now case-insensitive
(LOWER(value)) and stores normalized email on insert. Capital-letter
resubmissions no longer spawn duplicate client+yacht+interest rows.

Tier 5.6 + data-model H1: migration 0056 adds FK
user_permission_overrides.user_id → user(id) cascade, same for
user_port_roles.userId, plus partial unique index on
user_email_changes pending rows.

Tier 7.6: @types/node bumped from ^25 to ^20.19.0 — matches the runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 17:09:14 +02:00
parent 0baca41693
commit 16ef609e1b
16 changed files with 266 additions and 149 deletions

View File

@@ -78,3 +78,42 @@ export function renderShell({ title, body, branding }: ShellOpts): string {
export function brandingPrimaryColor(branding?: BrandingShell | null): string {
return branding?.primaryColor ?? DEFAULT_PRIMARY_COLOR;
}
/**
* URL-safe escaper for `href="..."` interpolations inside email
* templates. The email-deliverability audit flagged that every template
* inlined `${data.link}` directly into href + visible text without
* escaping — a `"` (or worse, a `javascript:` scheme) would break out
* of the attribute or trigger an XSS when the recipient opens the email
* in a webmail client that runs scripts.
*
* Two-step defense:
* 1. Scheme allow-list — only http(s), mailto, tel survive; everything
* else (javascript:, data:, vbscript:, file:, …) is rewritten to
* `about:blank`.
* 2. HTML-attribute escape on `"`, `<`, `>`, `&`, `'`, backtick.
*/
export function safeUrl(url: string | null | undefined): string {
if (!url) return 'about:blank';
const trimmed = String(url).trim();
// Block dangerous schemes. The allow-list is intentionally short.
const lower = trimmed.toLowerCase();
const ok =
lower.startsWith('http://') ||
lower.startsWith('https://') ||
lower.startsWith('mailto:') ||
lower.startsWith('tel:') ||
// Relative or root-relative paths are also acceptable — they
// resolve against the host the email links to (rare in transactional
// mail but used by tracking pixels and unsubscribe headers).
lower.startsWith('/') ||
lower.startsWith('#');
if (!ok) return 'about:blank';
return trimmed
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/`/g, '&#96;');
}