Skip to content
Open
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
3 changes: 2 additions & 1 deletion core/src/sessions/db/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import {MikroORM, Options as MikroORMOptions} from '@mikro-orm/core';
import {redactUriPassword} from '../../utils/redact_uri.js';
import {
ENTITIES,
SCHEMA_VERSION_1_JSON,
Expand Down Expand Up @@ -40,7 +41,7 @@ export async function getConnectionOptionsFromUri(
const {MsSqlDriver} = await import('@mikro-orm/mssql');
driver = MsSqlDriver;
} else {
throw new Error(`Unsupported database URI: ${uri}`);
throw new Error(`Unsupported database URI: ${redactUriPassword(uri)}`);
}

if (uri.startsWith('sqlite://')) {
Expand Down
3 changes: 2 additions & 1 deletion core/src/sessions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {redactUriPassword} from '../utils/redact_uri.js';
import {BaseSessionService} from './base_session_service.js';
import {
DatabaseSessionService,
Expand Down Expand Up @@ -32,5 +33,5 @@ export function getSessionServiceFromUri(uri: string): BaseSessionService {
return new VertexAiSessionService({});
}

throw new Error(`Unsupported session service URI: ${uri}`);
throw new Error(`Unsupported session service URI: ${redactUriPassword(uri)}`);
}
38 changes: 38 additions & 0 deletions core/src/utils/redact_uri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Redacts the password from a connection URI so the URI can be safely included
* in error messages and logs.
*
* A database or session-service connection URI such as
* `postgres://user:password@host:5432/db` embeds the password in its userinfo
* component. Including such a URI verbatim in a thrown Error or log entry leaks
* the credential to wherever those are collected (log files, error-tracking
* services, stdout captured by an orchestrator), which is frequently a
* different trust boundary from whoever provisioned the connection string.
*
* This masks the password while keeping the rest of the URI intact for
* debugging, mirroring the semantics of Go's `net/url.URL.Redacted()`.
*
* If the input cannot be parsed as a URL, only its scheme prefix is returned so
* that a credential embedded in an otherwise-unparseable string is not leaked.
*/
export function redactUriPassword(uri: string): string {
try {
const url = new URL(uri);
if (url.password) {
url.password = '***';
return url.toString();
}
return uri;
} catch {
const schemeEnd = uri.indexOf('://');
return schemeEnd === -1
? '<redacted>'
: `${uri.slice(0, schemeEnd)}://<redacted>`;
}
}
55 changes: 55 additions & 0 deletions core/test/utils/redact_uri_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {describe, expect, it} from 'vitest';
import {getConnectionOptionsFromUri} from '../../src/sessions/db/operations.js';
import {getSessionServiceFromUri} from '../../src/sessions/registry.js';
import {redactUriPassword} from '../../src/utils/redact_uri.js';

describe('redactUriPassword', () => {
it('masks the password while keeping the rest of the URI', () => {
expect(redactUriPassword('postgres://user:pass@db.host:5432/mydb')).toBe(
'postgres://user:***@db.host:5432/mydb',
);
});

it('masks the password for unsupported schemes too', () => {
expect(redactUriPassword('oracle://admin:hunter2@ora.host/xe')).toBe(
'oracle://admin:***@ora.host/xe',
);
});

it('leaves a URI without a password unchanged', () => {
expect(redactUriPassword('postgres://user@db.host/mydb')).toBe(
'postgres://user@db.host/mydb',
);
});

it('does not leak anything after the scheme for unparseable input', () => {
const out = redactUriPassword('not a url with :hunter2@ inside it');
expect(out).not.toContain('hunter2');
});
});

describe('connection-URI errors do not leak the password', () => {
it('getConnectionOptionsFromUri redacts the password in its error', async () => {
await expect(
getConnectionOptionsFromUri('oracle://admin:hunter2@ora.host/xe'),
).rejects.toThrow(/oracle:\/\/admin:\*\*\*@ora\.host\/xe/);
await expect(
getConnectionOptionsFromUri('oracle://admin:hunter2@ora.host/xe'),
).rejects.not.toThrow(/hunter2/);
});

it('getSessionServiceFromUri redacts the password in its error', () => {
expect(() =>
getSessionServiceFromUri('oracle://admin:hunter2@ora.host/xe'),
).toThrow(/oracle:\/\/admin:\*\*\*@ora\.host\/xe/);
expect(() =>
getSessionServiceFromUri('oracle://admin:hunter2@ora.host/xe'),
).not.toThrow(/hunter2/);
});
});
Loading