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

+10 -2
View File
@@ -1,4 +1,6 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { mkdir, readFile } from 'node:fs/promises'
import { writeFile } from '../atomic-write.js'
import { assertPublicHttpsUrl } from '../url-safety.js'
import { sanitizeSiteContent } from '../helpers.js'
import { requireAdminAuth, isValidAdminSession } from '../auth.js'
import {
@@ -524,12 +526,18 @@ export function register(app) {
res.json({ webhooks: state.webhooks })
})
app.post('/api/admin-webhooks', requireAdminAuth, (req, res) => {
app.post('/api/admin-webhooks', requireAdminAuth, async (req, res) => {
const { url, events } = req.body ?? {}
if (!url || typeof url !== 'string' || !/^https:\/\//i.test(url.trim())) {
res.status(400).json({ message: 'A valid HTTPS URL is required.' })
return
}
try {
await assertPublicHttpsUrl(url.trim())
} catch (err) {
res.status(400).json({ message: `Webhook URL rejected: ${err.message}` })
return
}
if (state.webhooks.length >= 50) {
res.status(400).json({ message: 'Maximum of 50 webhooks reached.' })
return