Skip to content

Commit d396a94

Browse files
committed
Use shell-quote to sanitize CDS extractor args
1 parent 1bd1466 commit d396a94

File tree

7 files changed

+162
-49
lines changed

7 files changed

+162
-49
lines changed

extractors/cds/tools/dist/cds-extractor.bundle.js

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

extractors/cds/tools/dist/cds-extractor.bundle.js.map

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

extractors/cds/tools/esbuild.config.mjs

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,54 @@ import { statSync } from 'fs';
22

33
import { build as esbuildFunc } from 'esbuild';
44

5+
const NODE_VERSION_TARGET = 'node18';
6+
57
const buildOptions = {
6-
entryPoints: ['cds-extractor.ts'],
8+
banner: {
9+
js: '#!/usr/bin/env node',
10+
},
711
bundle: true,
8-
platform: 'node',
9-
target: 'node18',
10-
outfile: 'dist/cds-extractor.bundle.js',
12+
conditions: ['node'],
13+
entryPoints: ['cds-extractor.ts'],
1114
external: [
1215
// Node.js built-in modules
13-
'fs',
14-
'path',
15-
'os',
16+
'assert',
17+
'buffer',
1618
'child_process',
17-
'util',
18-
'events',
19-
'stream',
20-
'url',
2119
'crypto',
22-
'process',
23-
'buffer',
24-
'assert',
25-
'module',
26-
'net',
27-
'tls',
20+
'events',
21+
'fs',
2822
'http',
2923
'https',
30-
'zlib',
24+
'module',
25+
'net',
26+
'os',
27+
'path',
28+
'process',
3129
'readline',
30+
'shell-quote',
31+
'stream',
32+
'tls',
33+
'url',
34+
'util',
3235
'worker_threads',
36+
'zlib',
3337
],
34-
minify: true,
35-
sourcemap: true,
3638
format: 'cjs',
37-
banner: {
38-
js: '#!/usr/bin/env node',
39-
},
40-
logLevel: 'info',
4139
// Handle TypeScript files
4240
loader: {
4341
'.ts': 'ts',
4442
},
45-
// Resolve TypeScript paths
46-
resolveExtensions: ['.ts', '.js'],
43+
logLevel: 'info',
4744
// Ensure proper module resolution
4845
mainFields: ['main', 'module'],
49-
conditions: ['node'],
46+
minify: true,
47+
outfile: 'dist/cds-extractor.bundle.js',
48+
platform: 'node',
49+
// Resolve TypeScript paths
50+
resolveExtensions: ['.ts'],
51+
sourcemap: true,
52+
target: NODE_VERSION_TARGET,
5053
};
5154

5255
async function build() {

extractors/cds/tools/src/packageManager/versionResolver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { execSync } from 'child_process';
22

3+
import { quote } from 'shell-quote';
4+
35
import type { SemanticVersion } from './types';
46
import { cdsExtractorLog } from '../logging';
57

@@ -140,7 +142,9 @@ export function getAvailableVersions(packageName: string): string[] {
140142
// Cache miss - fetch from npm
141143
cacheStats.misses++;
142144
try {
143-
const output = execSync(`npm view ${packageName} versions --json`, {
145+
// Use shell-quote to properly escape the package name to prevent injection
146+
const escapedPackageName = quote([packageName]);
147+
const output = execSync(`npm view ${escapedPackageName} versions --json`, {
144148
encoding: 'utf8',
145149
timeout: 30000, // 30 second timeout
146150
});

extractors/cds/tools/src/utils.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,38 @@
1+
import { resolve } from 'path';
2+
13
const USAGE_MESSAGE = `\tUsage: node <script> <source-root>`;
24

5+
/**
6+
* Resolves and validates a source root directory path.
7+
*
8+
* This function takes a source root path, validates it, normalizes it,
9+
* and returns an absolute path to the directory.
10+
*
11+
* @param sourceRoot - The source root path to resolve
12+
* @returns The normalized absolute path to the source root directory
13+
* @throws {Error} If the source root is null, undefined, an empty string,
14+
* or does not point to a valid directory
15+
*/
16+
function resolveSourceRoot(sourceRoot: string): string {
17+
// Check for null, undefined, or empty string
18+
if (!sourceRoot || typeof sourceRoot !== 'string') {
19+
throw new Error('Source root must be a non-empty string');
20+
}
21+
22+
// Normalize the path and resolve it to an absolute path.
23+
const normalizedPath = resolve(sourceRoot);
24+
25+
// Check if the resolved path points to a valid, existing directory.
26+
if (!normalizedPath || normalizedPath === '/') {
27+
throw new Error('Source root must point to a valid directory');
28+
}
29+
30+
return normalizedPath;
31+
}
32+
333
/**
434
* Check if the script was invoked with the required arguments.
535
* This function validates and sanitizes script arguments and returns them if valid.
6-
* The CDS extractor now runs in autobuild mode by default.
736
*
837
* Requirements:
938
* - Only requires: <source-root>
@@ -28,7 +57,18 @@ export function validateArguments(args: string[]): {
2857
}
2958

3059
// Get the source root from args (now the first parameter after script name)
31-
const sourceRoot: string = args[2];
60+
const rawSourceRoot: string = args[2];
61+
62+
// Validate and sanitize the source root path
63+
let sourceRoot: string;
64+
try {
65+
sourceRoot = resolveSourceRoot(rawSourceRoot);
66+
} catch (error) {
67+
return {
68+
isValid: false,
69+
usageMessage: `Invalid source root: ${String(error)}`,
70+
};
71+
}
3272

3373
// Return the validated arguments
3474
return {

extractors/cds/tools/test/src/utils.test.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
import { resolve } from 'path';
2+
13
import { validateArguments } from '../../src/utils';
24

5+
// Mock path module
6+
jest.mock('path', () => ({
7+
resolve: jest.fn(),
8+
}));
9+
10+
const mockedResolve = resolve as jest.MockedFunction<typeof resolve>;
11+
312
const EXTRACTOR_SCRIPT_NAME = 'cds-extractor.js';
413

514
describe('utils', () => {
@@ -9,18 +18,23 @@ describe('utils', () => {
918
beforeEach(() => {
1019
// Mock console.warn to avoid polluting test output
1120
console.warn = jest.fn();
21+
// Set up default mock behavior
22+
mockedResolve.mockImplementation((path: string) =>
23+
path.startsWith('/') ? path : `/absolute/${path}`,
24+
);
1225
});
1326

1427
afterEach(() => {
1528
console.warn = originalConsoleWarn;
29+
jest.clearAllMocks();
1630
});
1731

1832
it('should validate when source root is provided', () => {
1933
const args = ['node', EXTRACTOR_SCRIPT_NAME, 'source-root'];
2034
const result = validateArguments(args);
2135
expect(result.isValid).toBe(true);
2236
expect(result.args).toEqual({
23-
sourceRoot: 'source-root',
37+
sourceRoot: '/absolute/source-root',
2438
});
2539
});
2640

@@ -43,8 +57,57 @@ describe('utils', () => {
4357
const result = validateArguments(args);
4458
expect(result.isValid).toBe(true);
4559
expect(result.args).toEqual({
46-
sourceRoot: 'source-root',
60+
sourceRoot: '/absolute/source-root',
61+
});
62+
});
63+
64+
it('should handle source root with special characters', () => {
65+
const specialInputs = [
66+
'source-root;rm -rf /',
67+
'source-root|cat /etc/passwd',
68+
'source-root`whoami`',
69+
'source-root$(whoami)',
70+
'source-root{test}',
71+
'source-root<test>',
72+
'source-root*test',
73+
'source-root?test',
74+
'source-root~test',
75+
];
76+
77+
specialInputs.forEach(input => {
78+
const args = ['node', EXTRACTOR_SCRIPT_NAME, input];
79+
const result = validateArguments(args);
80+
expect(result.isValid).toBe(true);
81+
expect(result.args).toEqual({
82+
sourceRoot: `/absolute/${input}`,
83+
});
84+
});
85+
});
86+
87+
it('should handle source root with null bytes', () => {
88+
const args = ['node', EXTRACTOR_SCRIPT_NAME, 'source-root\0test'];
89+
const result = validateArguments(args);
90+
expect(result.isValid).toBe(true);
91+
expect(result.args).toEqual({
92+
sourceRoot: '/absolute/source-root\0test',
4793
});
4894
});
95+
96+
it('should reject empty or non-string source root', () => {
97+
const args = ['node', EXTRACTOR_SCRIPT_NAME, ''];
98+
const result = validateArguments(args);
99+
expect(result.isValid).toBe(false);
100+
expect(result.usageMessage).toContain('non-empty string');
101+
});
102+
103+
it('should reject source root that resolves to root directory', () => {
104+
// Mock path.resolve to return '/' to test the edge case
105+
mockedResolve.mockReturnValueOnce('/');
106+
107+
const args = ['node', EXTRACTOR_SCRIPT_NAME, 'some-path'];
108+
const result = validateArguments(args);
109+
expect(result.isValid).toBe(false);
110+
expect(result.usageMessage).toContain('valid directory');
111+
});
49112
});
50113
});

extractors/cds/tools/validate-bundle.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ const fs = require('fs');
44
const os = require('os');
55
const path = require('path');
66

7+
const DEFAULT_TIMEOUT_MS = 5000; // Default timeout for execution in milliseconds
8+
const MAX_BUNDLE_SIZE_MB = 50; // Set a reasonable max bundle size
9+
710
const bundlePath = path.join(__dirname, 'dist', 'cds-extractor.bundle.js');
811

9-
console.log('🔍 Validating CDS extractor bundle...');
12+
console.log('🔍 Validating CDS extractor bundle at', bundlePath);
1013

1114
// Check bundle exists
1215
if (!fs.existsSync(bundlePath)) {
@@ -21,7 +24,7 @@ const stats = fs.statSync(bundlePath);
2124
const sizeInMB = stats.size / (1024 * 1024);
2225
console.log(`📦 Bundle size: ${sizeInMB.toFixed(2)} MB`);
2326

24-
if (sizeInMB > 50) {
27+
if (sizeInMB > MAX_BUNDLE_SIZE_MB) {
2528
console.warn('⚠️ Bundle size is quite large, consider optimizing');
2629
}
2730

@@ -51,7 +54,7 @@ try {
5154
execSync(nodeCmd, {
5255
stdio: 'pipe',
5356
cwd: testDir,
54-
timeout: 5000, // 5 second timeout
57+
timeout: DEFAULT_TIMEOUT_MS,
5558
encoding: 'utf8',
5659
});
5760
console.log('✅ Bundle execution completed successfully');

0 commit comments

Comments
 (0)