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

+26
View File
@@ -0,0 +1,26 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { mkdtemp, readFile, readdir, writeFile as plainWrite, rm } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { writeFile } from '../server/atomic-write.js'
test('replaces file contents and leaves no temp files', async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), 'atomic-'))
const file = path.join(dir, 'a.json')
await plainWrite(file, 'old')
await writeFile(file, '{"ok":true}', 'utf8')
assert.equal(await readFile(file, 'utf8'), '{"ok":true}')
assert.deepEqual(await readdir(dir), ['a.json'])
await rm(dir, { recursive: true })
})
test('a failed write keeps the old file and cleans up', async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), 'atomic-'))
const file = path.join(dir, 'a.json')
await plainWrite(file, 'old')
await assert.rejects(() => writeFile(file, { not: 'a string' }))
assert.equal(await readFile(file, 'utf8'), 'old')
assert.deepEqual(await readdir(dir), ['a.json'])
await rm(dir, { recursive: true })
})
+17
View File
@@ -0,0 +1,17 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { isPrivateAddress, assertPublicHttpsUrl } from '../server/url-safety.js'
test('private, loopback, link-local and tailscale addresses are flagged', () => {
for (const ip of ['127.0.0.1', '10.0.0.5', '192.168.1.1', '172.16.0.1', '169.254.169.254', '100.64.1.1', '::1', 'fd00::1', 'fe80::1', '::ffff:10.0.0.1']) {
assert.ok(isPrivateAddress(ip), ip)
}
for (const ip of ['8.8.8.8', '1.1.1.1', '2606:4700:4700::1111']) assert.ok(!isPrivateAddress(ip), ip)
})
test('assertPublicHttpsUrl rejects unsafe URLs', async () => {
for (const url of ['http://example.com', 'https://localhost/x', 'https://127.0.0.1/', 'https://169.254.169.254/latest', 'https://user:pw@example.com', 'https://nas.local/hook', 'not a url']) {
await assert.rejects(() => assertPublicHttpsUrl(url), url)
}
await assert.doesNotReject(() => assertPublicHttpsUrl('https://8.8.8.8/hook'))
})