Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

### A malformed `DATABASE_URL` is refused without printing the password

`DATABASE_URL` is taken apart before it reaches Bun, and the string most likely to fail that parse is
one with a stray character in the password. The refusal for an unparseable value quoted the whole
string back to name the fault, which wrote the database password into the log line that reported it.
It now names the variable and the shape it expects, the way the other refusals beside it already do,
and never echoes the value.

### Wiping a computer is recorded even if clearing its stored state fails

Resetting a computer destroys the profile first and wrote the audit row last, after two Postgres
Expand Down
6 changes: 5 additions & 1 deletion server/src/db/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ function addressOf(databaseUrl: string) {
try {
url = new URL(databaseUrl);
} catch {
// The value is not echoed back. DATABASE_URL holds the database password, and the one string
// most likely to fail `new URL` is one with a stray character in that password, so printing it
// to name the fault would put the credential in the log line that reports it. Name the variable
// and the shape it expects, the way every other refusal in this function does.
throw new TypeError(
`DATABASE_URL is not a URL: ${JSON.stringify(databaseUrl)}`,
"DATABASE_URL is not a valid URL. Expected postgres://user:password@host:port/database.",
);
}
if (url.hostname === "") {
Expand Down
20 changes: 18 additions & 2 deletions server/tests/db-client-address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,28 @@ describe("the database address", () => {
expect(process.env.DATABASE_URL).toBeUndefined();
});

test("refuses a connection string that is not a URL, naming what it got", () => {
test("refuses a connection string that is not a URL", () => {
expect(() => createDatabase("://openbot@/openbot")).toThrow(
/DATABASE_URL is not a URL/,
/DATABASE_URL is not a valid URL/,
);
});

test("does not put the password in the message when the URL will not parse", () => {
// A stray character in a generated password is the likeliest reason `new URL` throws here, so
// the refusal must not echo the string it was given: DATABASE_URL carries the credential, and a
// message quoting it would write the password into the log line that reports the fault. The
// invalid port makes `new URL` throw with the secret still present in the input.
const secret = "s3cr3t-p4ssw0rd";
let message = "";
try {
createDatabase(`postgres://openbot:${secret}@127.0.0.1:notaport/openbot`);
} catch (error) {
message = error instanceof Error ? error.message : String(error);
}
expect(message).toMatch(/DATABASE_URL is not a valid URL/);
expect(message).not.toContain(secret);
});

test("refuses a URL with no host, which would otherwise parse and connect nowhere", () => {
// `new URL` accepts this: the scheme is "openbot:" and there is no host at all.
expect(() => createDatabase("openbot:openbot@localhost/openbot")).toThrow(
Expand Down