diff --git a/CHANGELOG.md b/CHANGELOG.md index 6574458d3..d9ae571f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### A `DATABASE_URL` with a port of zero is refused at start-up + +`postgres://…:0/…` parsed and booted, and every query then failed against a port nothing listens +on. Ports outside 1-65535 are refused with a sentence naming `DATABASE_URL` before a socket is +ever opened. + ### `bun run dev` no longer starts a routines worker that cannot start `bun run dev` fanned out across every workspace, and one of them is the routines worker. That worker diff --git a/server/src/db/client.ts b/server/src/db/client.ts index 74c317685..5248a4bf6 100644 --- a/server/src/db/client.ts +++ b/server/src/db/client.ts @@ -69,10 +69,28 @@ function addressOf(databaseUrl: string) { */ const connection = Object.fromEntries(url.searchParams); + /* + * A port that is not a port is refused before a socket is ever opened. + * + * `new URL` already rejects `:65536` and above, but `:0` parses to `"0"` and would travel + * into `new SQL({ port: 0 })` as `0`. Boot then succeeds and every query fails against a port + * nothing listens on, instead of the start-up refusal every other malformed address here gets. + * Postgres ports are 1-65535, the same range the server's own `PORT`/`SERVER_PORT` enforces. + */ + let port = 5432; + if (url.port !== "") { + port = Number(url.port); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new TypeError( + "DATABASE_URL names a port that is not between 1 and 65535.", + ); + } + } + return { adapter: "postgres" as const, hostname: url.hostname, - port: url.port === "" ? 5432 : Number(url.port), + port, username: decodePart(url.username, "username"), password: decodePart(url.password, "password"), database, diff --git a/server/tests/db-client-address.test.ts b/server/tests/db-client-address.test.ts index 13c71ac4e..bf420e8e7 100644 --- a/server/tests/db-client-address.test.ts +++ b/server/tests/db-client-address.test.ts @@ -62,6 +62,17 @@ describe("the database address", () => { ).toThrow(/names no database/); }); + test("refuses a port of zero instead of connecting nowhere", () => { + /* + * `new URL` accepts `:0` and reports the port as `"0"`, so without this check boot succeeds + * and every query fails against a port nothing listens on. A refusal here names the variable + * and the range, the way every other malformed address does. + */ + expect(() => + createDatabase("postgres://openbot:openbot@127.0.0.1:0/openbot"), + ).toThrow(/DATABASE_URL names a port that is not between 1 and 65535/); + }); + test("refuses a password holding a percent that starts no escape, naming the part", () => { /* * `new URL` accepts this and `decodeURIComponent` does not, so the refusal used to be a bare