Skip to content

Commit c145b9e

Browse files
committed
Address allowlist and enum fallback reviews
1 parent deb98ca commit c145b9e

4 files changed

Lines changed: 58 additions & 21 deletions

File tree

src/server/connection-target.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ describe('connection target allowlist helpers', () => {
3232
)).not.toThrow();
3333
});
3434

35+
it('rejects URL host override parameters before allowlist matching', () => {
36+
const allowedTargets = normalizeAllowedConnectionTargets(['allowed.internal']);
37+
38+
expect(() => assertConnectionTargetAllowed(
39+
'postgresql://allowed.internal/app?host=evil.internal',
40+
allowedTargets
41+
)).toThrow('host or hostaddr query parameters');
42+
expect(() => assertConnectionTargetAllowed(
43+
'postgresql://allowed.internal/app?hostaddr=10.0.0.1',
44+
allowedTargets
45+
)).toThrow('host or hostaddr query parameters');
46+
});
47+
3548
it('parses keyword-style connection targets', () => {
3649
expect(parseConnectionTarget("host=db.internal port=5432 dbname=app user='read only' password=secret")).toEqual({
3750
host: 'db.internal',

src/server/connection-target.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ export function parseConnectionTarget(connectionString: string): ConnectionTarge
211211
if (!parsed.hostname) {
212212
throw new Error('Connection target allowlist requires URL connection strings to include an explicit host.');
213213
}
214+
if (parsed.searchParams.has('host') || parsed.searchParams.has('hostaddr')) {
215+
throw new Error('Connection target allowlist does not support URL host or hostaddr query parameters.');
216+
}
214217

215218
return {
216219
host: normalizeConnectionHost(parsed.hostname),
@@ -220,7 +223,7 @@ export function parseConnectionTarget(connectionString: string): ConnectionTarge
220223
};
221224
}
222225
} catch (error) {
223-
if (error instanceof Error && error.message.includes('allowlist requires')) {
226+
if (error instanceof Error && error.message.includes('allowlist')) {
224227
throw error;
225228
}
226229
}

src/tools/schema.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,27 @@ describe('manageSchemaTools', () => {
383383
expect(output).toContain('"active"');
384384
});
385385

386+
it('passes undefined connection strings through enum fallback paths', async () => {
387+
const mockDb = {
388+
connect: vi.fn().mockResolvedValue(undefined),
389+
query: vi.fn()
390+
.mockResolvedValueOnce([])
391+
.mockResolvedValueOnce([]),
392+
disconnect: vi.fn().mockResolvedValue(undefined)
393+
};
394+
vi.spyOn(DatabaseConnection, 'getInstance').mockReturnValue(mockDb as unknown as DatabaseConnection);
395+
396+
await manageSchemaTools.execute({ operation: 'get_enums' }, mockGetConnectionString);
397+
await manageSchemaTools.execute({
398+
operation: 'create_enum',
399+
enumName: 'status',
400+
values: ['active']
401+
}, mockGetConnectionString);
402+
403+
expect(mockGetConnectionString).toHaveBeenNthCalledWith(1, undefined);
404+
expect(mockGetConnectionString).toHaveBeenNthCalledWith(2, undefined);
405+
});
406+
386407
it('sanitizes schema database errors before returning them', async () => {
387408
const mockDb = {
388409
connect: vi.fn().mockResolvedValue(undefined),

src/tools/schema.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,11 @@ async function getTableInfo(db: DatabaseConnection, tableName: string, schema: s
410410
}
411411

412412
// Enum functions (adapted from enums.ts)
413-
async function executeGetEnumsInSchema(
414-
connectionString: string,
415-
schema = 'public',
416-
enumName?: string,
417-
getConnectionString?: GetConnectionStringFn
413+
async function executeGetEnumsInSchema(
414+
connectionString: string | undefined,
415+
schema = 'public',
416+
enumName?: string,
417+
getConnectionString?: GetConnectionStringFn
418418
): Promise<EnumInfo[]> {
419419
const resolvedConnectionString = getConnectionString ? getConnectionString(connectionString) : connectionString;
420420
const db = DatabaseConnection.getInstance();
@@ -450,9 +450,9 @@ async function executeGetEnumsInSchema(
450450
}
451451

452452
async function executeCreateEnumInSchema(
453-
connectionString: string,
454-
enumName: string,
455-
values: string[],
453+
connectionString: string | undefined,
454+
enumName: string,
455+
values: string[],
456456
schema = 'public',
457457
ifNotExists = false,
458458
getConnectionString?: GetConnectionStringFn
@@ -562,12 +562,12 @@ export const manageSchemaTools: PostgresTool = {
562562
return { content: [{ type: 'text', text: `Table ${result.tableName} altered successfully.` }, { type: 'text', text: JSON.stringify(result, null, 2) }] };
563563
}
564564

565-
case 'get_enums': {
566-
const result = await executeGetEnumsInSchema(
567-
connStringArg || '',
568-
schema || 'public',
569-
enumName,
570-
getConnectionStringVal
565+
case 'get_enums': {
566+
const result = await executeGetEnumsInSchema(
567+
connStringArg,
568+
schema || 'public',
569+
enumName,
570+
getConnectionStringVal
571571
);
572572
return { content: [{ type: 'text', text: `Fetched ${result.length} ENUM(s).` }, { type: 'text', text: JSON.stringify(result, null, 2) }] };
573573
}
@@ -578,12 +578,12 @@ export const manageSchemaTools: PostgresTool = {
578578
content: [{ type: 'text', text: 'Error: enumName and values are required for create_enum operation' }],
579579
isError: true
580580
};
581-
}
582-
const result = await executeCreateEnumInSchema(
583-
connStringArg || '',
584-
enumName,
585-
values,
586-
schema || 'public',
581+
}
582+
const result = await executeCreateEnumInSchema(
583+
connStringArg,
584+
enumName,
585+
values,
586+
schema || 'public',
587587
ifNotExists || false,
588588
getConnectionStringVal
589589
);

0 commit comments

Comments
 (0)