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

+16
View File
@@ -84,6 +84,10 @@ app.use((_req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff')
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()')
if (process.env.NODE_ENV === 'production') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000')
}
res.setHeader(
'Content-Security-Policy',
[
@@ -147,6 +151,18 @@ app.use((req, res, next) => {
// Public routes (robots, sitemap, static, SPA fallback)
registerPublic(app)
// JSON error responses for API routes instead of Express's default HTML page.
app.use((err, req, res, _next) => {
if (res.headersSent) return
const status = Number.isInteger(err?.status) && err.status >= 400 && err.status < 600 ? err.status : 500
if (status >= 500) console.error('[error]', req.method, req.path, err)
if (req.path.startsWith('/api/')) {
res.status(status).json({ message: status >= 500 ? 'Internal server error.' : String(err?.message ?? 'Request failed.') })
return
}
res.status(status).type('text/plain').send(status >= 500 ? 'Internal server error.' : 'Bad request.')
})
const PORT = Number(process.env.PORT ?? 4173)
Promise.all([
loadHitStatsFromDisk(),