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
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:
1 parent
8a796964b9
commit
2a64f29a00
18 files changed
+209
-17
No files matched your search
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user