All checks were successful
CI / checks (pull_request) Successful in 34s
POST /notify/:id accepted an attacker-supplied relay URL with a non-ws://
scheme (e.g. http://…). The URL was passed straight to @welshman/net's
load(), whose internal batcher throws 'Invalid relay url' asynchronously
(via setTimeout). That error escaped the route's try/catch as an unhandled
rejection, triggering process.exit(1) in src/index.ts — the entire server
died. Any registered subscriber could crash the service repeatedly.
Two fixes applied (either alone breaks the attack):
1. Validate relay scheme in the route handler before calling load()
(src/server.ts). Uses isRelayUrl() from @welshman/util, which accepts
only wss:// and ws:// schemes. Returns 400 immediately for invalid
URLs, preventing the bad URL from ever reaching the batcher.
2. Soften process.on('unhandledRejection') in src/index.ts to log and
continue instead of calling process.exit(1). Defence in depth: if any
other async error escapes a try/catch, the server stays up.
Test: test/notify-non-wss-relay-crash.test.ts — starts an ephemeral
server, creates a confirmed subscription, POSTs with http://127.0.0.1:9,
expects 400 with an error message.
29 lines
No EOL
877 B
TypeScript
29 lines
No EOL
877 B
TypeScript
import 'localstorage-polyfill'
|
|
|
|
import { PORT } from './env.js'
|
|
import { server } from './server.js'
|
|
import { migrate, getActiveSubscriptions } from './database.js'
|
|
import { registerSubscription } from './worker/index.js'
|
|
|
|
process.on('unhandledRejection', (error: Error) => {
|
|
console.error('Unhandled rejection:', error.stack)
|
|
// Do not process.exit(1) — an async rejection from a library's internal
|
|
// timer (e.g. @welshman/net's batcher) would let an attacker crash the
|
|
// entire server with a single malformed request. Log and continue.
|
|
})
|
|
|
|
process.on('uncaughtException', (error: Error) => {
|
|
console.error('Uncaught exception:', error.stack)
|
|
process.exit(1)
|
|
})
|
|
|
|
|
|
migrate().then(async () => {
|
|
server.listen(PORT, () => {
|
|
console.log('Running on port', PORT)
|
|
})
|
|
|
|
for (const sub of await getActiveSubscriptions()) {
|
|
registerSubscription(sub)
|
|
}
|
|
}) |