Skip to content

Commit dc686f5

Browse files
fix(shared): harden direct URL loading and align lodash to 4.18.1 (#2304)
Restrict DirectUrlDocumentLoader to approved hosts, block URL credentials, and force relative request paths to reduce SSRF risk (CodeQL alert 68). Add targeted tests and loader option wiring. Also align lodash/lodash-es to 4.18.1 in overrides and regenerate lockfiles on Node 22, including advent-of-calm website. Follow-up: merge allowedRemoteHosts from CLI user config in parseDocumentLoaderConfig and add coverage tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 87b44b6 commit dc686f5

12 files changed

Lines changed: 718 additions & 646 deletions

advent-of-calm/website/package-lock.json

Lines changed: 231 additions & 318 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

advent-of-calm/website/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"diff": "^8.0.3",
77
"ajv": "^8.18.0",
88
"devalue": "^5.6.3",
9-
"lodash": "^4.17.23",
9+
"lodash": "^4.18.1",
1010
"yaml": "^2.8.3"
1111
},
1212
"scripts": {

cli/src/cli-config.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ vi.mock('os', () => ({
1212
}));
1313

1414
const exampleConfig = {
15-
calmHubUrl: 'https://example.com/calmhub'
15+
calmHubUrl: 'https://example.com/calmhub',
16+
allowedRemoteHosts: ['schemas.example.com']
1617
};
1718

1819

@@ -44,4 +45,4 @@ describe('cli-config', () => {
4445
});
4546
await expect(loadCliConfig()).resolves.toBeNull();
4647
});
47-
});
48+
});

cli/src/cli-config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { join } from 'path';
55

66
export interface CLIConfig {
77
calmHubUrl?: string
8+
allowedRemoteHosts?: string[]
89
}
910

1011
function getUserConfigLocation(): string {
@@ -30,4 +31,4 @@ export async function loadCliConfig(): Promise<CLIConfig | null> {
3031
logger.error('Unexpected error loading user config: ' + String(err));
3132
return null;
3233
}
33-
}
34+
}

cli/src/cli.spec.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
} from '@finos/calm-shared';
88
import { Command } from 'commander';
99
import { MockInstance } from 'vitest';
10-
import { parseDocumentLoaderConfig } from './cli';
1110

1211
let calmShared: typeof import('@finos/calm-shared');
1312
let validateModule: typeof import('./command-helpers/validate');
@@ -440,8 +439,18 @@ describe('CLI Commands', () => {
440439
});
441440

442441
describe('parseDocumentLoaderConfig', () => {
442+
const parseDocLoaderConfigForTest = async (options: {
443+
verbose?: boolean;
444+
calmHubUrl?: string;
445+
schemaDirectory?: string;
446+
allowedRemoteHosts?: string[];
447+
}) => {
448+
const cliModule = await import('./cli');
449+
return cliModule.parseDocumentLoaderConfig(options);
450+
};
451+
443452
it('should parse calmhub url when provided', async () => {
444-
const options = await parseDocumentLoaderConfig({
453+
const options = await parseDocLoaderConfigForTest({
445454
calmHubUrl: 'calmhub'
446455
});
447456
expect(options.calmHubUrl).toEqual('calmhub');
@@ -451,29 +460,58 @@ describe('parseDocumentLoaderConfig', () => {
451460
cliConfigModule = await import('./cli-config');
452461
vi.spyOn(cliConfigModule, 'loadCliConfig').mockResolvedValue({ calmHubUrl: 'calmhub-file' });
453462

454-
const options = await parseDocumentLoaderConfig({
463+
const options = await parseDocLoaderConfigForTest({
455464
calmHubUrl: 'calmhub-cli'
456465
});
457466
expect(options.calmHubUrl).toEqual('calmhub-cli');
458467
});
459468

460469
it('should parse schemaDirectoryPath when provided', async () => {
461-
const options = await parseDocumentLoaderConfig({
470+
const options = await parseDocLoaderConfigForTest({
462471
schemaDirectory: 'path'
463472
});
464473
expect(options.schemaDirectoryPath).toEqual('path');
465474
});
466475

476+
it('should parse allowedRemoteHosts when provided', async () => {
477+
const options = await parseDocLoaderConfigForTest({
478+
allowedRemoteHosts: ['schemas.example.com']
479+
});
480+
expect(options.allowedRemoteHosts).toEqual(['schemas.example.com']);
481+
});
482+
483+
it('should use allowedRemoteHosts from config when CLI does not provide them', async () => {
484+
cliConfigModule = await import('./cli-config');
485+
vi.spyOn(cliConfigModule, 'loadCliConfig').mockResolvedValue({
486+
allowedRemoteHosts: ['config.example.com']
487+
});
488+
489+
const options = await parseDocLoaderConfigForTest({});
490+
expect(options.allowedRemoteHosts).toEqual(['config.example.com']);
491+
});
492+
493+
it('should prefer CLI allowedRemoteHosts over config values', async () => {
494+
cliConfigModule = await import('./cli-config');
495+
vi.spyOn(cliConfigModule, 'loadCliConfig').mockResolvedValue({
496+
allowedRemoteHosts: ['config.example.com']
497+
});
498+
499+
const options = await parseDocLoaderConfigForTest({
500+
allowedRemoteHosts: ['cli.example.com']
501+
});
502+
expect(options.allowedRemoteHosts).toEqual(['cli.example.com']);
503+
});
504+
467505
it('should set debug to true when verbose passed along', async () => {
468-
const options = await parseDocumentLoaderConfig({
506+
const options = await parseDocLoaderConfigForTest({
469507
verbose: true
470508
});
471509
expect(options.debug).toBeTruthy();
472510
});
473511

474512
it('should default debug to false', async () => {
475-
const options = await parseDocumentLoaderConfig({
513+
const options = await parseDocLoaderConfigForTest({
476514
});
477515
expect(options.debug).toBeFalsy();
478516
});
479-
});
517+
});

cli/src/cli.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CALM_META_SCHEMA_DIRECTORY, DocifyMode, initLogger, runGenerate, Schema
22
import { Option, Command } from 'commander';
33
import { version } from '../package.json';
44
import { promptUserForOptions } from './command-helpers/generate-options';
5-
import { loadCliConfig } from './cli-config';
5+
import * as cliConfig from './cli-config';
66
import path from 'path';
77
import { select } from '@inquirer/prompts';
88

@@ -245,6 +245,7 @@ interface ParseDocumentLoaderOptions {
245245
verbose?: boolean;
246246
calmHubUrl?: string;
247247
schemaDirectory?: string;
248+
allowedRemoteHosts?: string[];
248249
}
249250

250251
export async function parseDocumentLoaderConfig(
@@ -258,17 +259,22 @@ export async function parseDocumentLoaderConfig(
258259
schemaDirectoryPath: options.schemaDirectory,
259260
urlToLocalMap: urlToLocalMap,
260261
basePath: basePath,
262+
allowedRemoteHosts: options.allowedRemoteHosts,
261263
debug: !!options.verbose
262264
};
263265

264-
const userConfig = await loadCliConfig();
266+
const userConfig = await cliConfig.loadCliConfig();
265267
if (userConfig && userConfig.calmHubUrl && !options.calmHubUrl) {
266268
logger.info('Using CALMHub URL from config file: ' + userConfig.calmHubUrl);
267269
docLoaderOpts.calmHubUrl = userConfig.calmHubUrl;
268270
}
271+
if (userConfig && userConfig.allowedRemoteHosts && !options.allowedRemoteHosts) {
272+
logger.info('Using allowed remote hosts from config file');
273+
docLoaderOpts.allowedRemoteHosts = userConfig.allowedRemoteHosts;
274+
}
269275
return docLoaderOpts;
270276
}
271277

272278
export async function buildSchemaDirectory(docLoader: DocumentLoader, debug: boolean): Promise<SchemaDirectory> {
273279
return new SchemaDirectory(docLoader, debug);
274-
}
280+
}

0 commit comments

Comments
 (0)