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>
51 lines
2.0 KiB
JavaScript
51 lines
2.0 KiB
JavaScript
import { lookup } from 'node:dns/promises'
|
|
import net from 'node:net'
|
|
|
|
function isPrivateIPv4(ip) {
|
|
const [a, b] = ip.split('.').map(Number)
|
|
return (
|
|
a === 0 || a === 10 || a === 127
|
|
|| (a === 100 && b >= 64 && b <= 127) // CGNAT / Tailscale
|
|
|| (a === 169 && b === 254) // link-local, cloud metadata
|
|
|| (a === 172 && b >= 16 && b <= 31)
|
|
|| (a === 192 && b === 168)
|
|
|| a >= 224 // multicast / reserved
|
|
)
|
|
}
|
|
|
|
export function isPrivateAddress(ip) {
|
|
if (net.isIPv4(ip)) return isPrivateIPv4(ip)
|
|
if (net.isIPv6(ip)) {
|
|
const lower = ip.toLowerCase()
|
|
if (lower === '::' || lower === '::1') return true
|
|
if (lower.startsWith('fc') || lower.startsWith('fd')) return true // unique local
|
|
if (/^fe[89ab]/.test(lower)) return true // link-local
|
|
const mapped = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)
|
|
if (mapped) return isPrivateIPv4(mapped[1])
|
|
return false
|
|
}
|
|
return true // not an IP at all: treat as unsafe
|
|
}
|
|
|
|
// Throws unless `rawUrl` is an https URL whose host resolves only to public
|
|
// addresses. Called when a webhook is registered and again before each delivery
|
|
// (DNS can change between the two).
|
|
export async function assertPublicHttpsUrl(rawUrl) {
|
|
let parsed
|
|
try {
|
|
parsed = new URL(rawUrl)
|
|
} catch {
|
|
throw new Error('Malformed URL.')
|
|
}
|
|
if (parsed.protocol !== 'https:') throw new Error('Only https:// URLs are allowed.')
|
|
if (parsed.username || parsed.password) throw new Error('URLs with embedded credentials are not allowed.')
|
|
const host = parsed.hostname.replace(/^\[|\]$/g, '')
|
|
if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local') || host.endsWith('.internal')) {
|
|
throw new Error('Internal hostnames are not allowed.')
|
|
}
|
|
const addresses = net.isIP(host) ? [{ address: host }] : await lookup(host, { all: true })
|
|
if (addresses.length === 0 || addresses.some(a => isPrivateAddress(a.address))) {
|
|
throw new Error('URL resolves to a private or internal address.')
|
|
}
|
|
}
|