Second hardening pass: atomic data writes, bible-passage limits, webhook SSRF guard, note sanitizing, CI gates; v2.10.1
Publish Container / build (push) Successful in 15m46s

- Atomic temp-file + rename for all JSON data writes (no truncated files on crash)
- /api/bible-passage: rate limit, range caps (60 verses), bounded cache
- Admin webhooks: https-only, block private/internal/Tailscale addresses (checked at registration and delivery), no redirects
- Resend webhook token compare is timing-safe
- DOMPurify on rendered study notes and broadcast preview
- CI runs lint + tests before build
- Permissions-Policy and HSTS (production) headers; JSON error handler for /api
- Tests for URL safety and atomic writes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-09 09:00:28 -04:00
1 parent 8a796964b9
commit 2a64f29a00
18 files changed
+209 -17

No files matched your search

+7 -3
View File
@@ -1,5 +1,7 @@
import { migrateScriptKeys } from './episode-script-keys.js'
import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
import { mkdir, readFile, readdir, stat, unlink } from 'node:fs/promises'
import { writeFile } from './atomic-write.js'
import { assertPublicHttpsUrl } from './url-safety.js'
import path from 'node:path'
import { randomUUID } from 'node:crypto'
import { sanitizeSiteContent } from './helpers.js'
@@ -533,12 +535,14 @@ export async function fireWebhooks(eventType, payload) {
if (targets.length === 0) return
const body = JSON.stringify({ event: eventType, data: payload, firedAt: new Date().toISOString() })
for (const wh of targets) {
fetch(wh.url, {
// Re-check at delivery time: DNS may have changed since the webhook was registered.
assertPublicHttpsUrl(wh.url).then(() => fetch(wh.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'Siteforge-Webhook/1.0' },
body,
redirect: 'manual',
signal: AbortSignal.timeout(8000),
})
}))
.then(r => {
appendAuditEntry('webhook-fired', `${eventType} → ${wh.url} (${r.status})`)
})