fix: reject curl destination override flags
* fix: reject curl destination override flags * fix: block additional curl target bypass forms
This commit is contained in:
2 files changed
+113
-2
No files matched your search
@@ -4,14 +4,18 @@ import { join } from 'path';
|
|||||||
|
|
||||||
const serverSource = readFileSync(join(process.cwd(), 'src/server.ts'), 'utf8');
|
const serverSource = readFileSync(join(process.cwd(), 'src/server.ts'), 'utf8');
|
||||||
|
|
||||||
function routeBlock(startMarker: string, endMarker: string): string {
|
function sourceBlock(startMarker: string, endMarker: string): string {
|
||||||
const start = serverSource.indexOf(startMarker);
|
const start = serverSource.indexOf(startMarker);
|
||||||
expect(start, `missing route marker ${startMarker}`).toBeGreaterThanOrEqual(0);
|
expect(start, `missing source marker ${startMarker}`).toBeGreaterThanOrEqual(0);
|
||||||
const end = serverSource.indexOf(endMarker, start);
|
const end = serverSource.indexOf(endMarker, start);
|
||||||
expect(end, `missing end marker ${endMarker}`).toBeGreaterThan(start);
|
expect(end, `missing end marker ${endMarker}`).toBeGreaterThan(start);
|
||||||
return serverSource.slice(start, end);
|
return serverSource.slice(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeBlock(startMarker: string, endMarker: string): string {
|
||||||
|
return sourceBlock(startMarker, endMarker);
|
||||||
|
}
|
||||||
|
|
||||||
describe('local API authorization hardening invariants', () => {
|
describe('local API authorization hardening invariants', () => {
|
||||||
it('/api/events does not grant wildcard CORS and rejects foreign browser origins before opening SSE', () => {
|
it('/api/events does not grant wildcard CORS and rejects foreign browser origins before opening SSE', () => {
|
||||||
const route = routeBlock("app.get('/api/events'", '// =============================================================================\n// API ENDPOINTS - HEALTH & STATUS');
|
const route = routeBlock("app.get('/api/events'", '// =============================================================================\n// API ENDPOINTS - HEALTH & STATUS');
|
||||||
@@ -30,6 +34,31 @@ describe('local API authorization hardening invariants', () => {
|
|||||||
expect(route).toMatch(/guardAction\(body,\s*['"]command_execution['"],\s*targetResolution\.target/);
|
expect(route).toMatch(/guardAction\(body,\s*['"]command_execution['"],\s*targetResolution\.target/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('/api/tools/execute rejects curl flags that change the effective destination', () => {
|
||||||
|
const parser = sourceBlock('const CURL_TRANSPORT_OVERRIDE_FLAGS', 'function inferCommandTarget');
|
||||||
|
|
||||||
|
for (const flag of [
|
||||||
|
'--resolve',
|
||||||
|
'--connect-to',
|
||||||
|
'--proxy',
|
||||||
|
'--preproxy',
|
||||||
|
'--socks5',
|
||||||
|
'--socks5-hostname',
|
||||||
|
'--unix-socket',
|
||||||
|
'--interface',
|
||||||
|
'--url',
|
||||||
|
'--config',
|
||||||
|
'--next',
|
||||||
|
'-K',
|
||||||
|
]) {
|
||||||
|
expect(parser).toContain(`'${flag}'`);
|
||||||
|
}
|
||||||
|
expect(parser).toMatch(/findCurlTransportOverrideFlag\(args\)/);
|
||||||
|
expect(parser).toMatch(/countCurlUrlOperands\(args\)\s*>\s*1/);
|
||||||
|
expect(parser).toMatch(/multiple URL operands/);
|
||||||
|
expect(parser).toMatch(/changes the effective network destination/);
|
||||||
|
});
|
||||||
|
|
||||||
it('Admiral live launch re-checks every General-produced execution target before mission bring-up', () => {
|
it('Admiral live launch re-checks every General-produced execution target before mission bring-up', () => {
|
||||||
const route = routeBlock("app.post('/api/admiral/launch'", '// =============================================================================\n// BOUNTY PLATFORM INTEGRATIONS');
|
const route = routeBlock("app.post('/api/admiral/launch'", '// =============================================================================\n// BOUNTY PLATFORM INTEGRATIONS');
|
||||||
|
|
||||||
|
|||||||
@@ -344,6 +344,79 @@ interface ParsedCommand {
|
|||||||
|
|
||||||
const SHELL_META = /[|&;$<>`\\]/;
|
const SHELL_META = /[|&;$<>`\\]/;
|
||||||
const COMMAND_CONTROL = /[\x00-\x1F\x7F-\x9F\u2028\u2029]/;
|
const COMMAND_CONTROL = /[\x00-\x1F\x7F-\x9F\u2028\u2029]/;
|
||||||
|
const CURL_TRANSPORT_OVERRIDE_FLAGS = new Set([
|
||||||
|
'--resolve',
|
||||||
|
'--connect-to',
|
||||||
|
'--proxy',
|
||||||
|
'--preproxy',
|
||||||
|
'--socks4',
|
||||||
|
'--socks4a',
|
||||||
|
'--socks5',
|
||||||
|
'--socks5-hostname',
|
||||||
|
'--unix-socket',
|
||||||
|
'--abstract-unix-socket',
|
||||||
|
'--interface',
|
||||||
|
'--url',
|
||||||
|
'--config',
|
||||||
|
'--next',
|
||||||
|
'-x',
|
||||||
|
'-K',
|
||||||
|
]);
|
||||||
|
const CURL_VALUE_FLAGS = new Set([
|
||||||
|
'-A', '--user-agent',
|
||||||
|
'-b', '--cookie',
|
||||||
|
'-c', '--cookie-jar',
|
||||||
|
'-d', '--data', '--data-ascii', '--data-binary', '--data-raw', '--data-urlencode',
|
||||||
|
'-F', '--form',
|
||||||
|
'-H', '--header',
|
||||||
|
'-m', '--max-time',
|
||||||
|
'-o', '--output',
|
||||||
|
'-T', '--upload-file',
|
||||||
|
'-u', '--user',
|
||||||
|
'-X', '--request',
|
||||||
|
'--cacert', '--cert', '--connect-timeout', '--key', '--request-target', '--retry',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function findCurlTransportOverrideFlag(args: string[]): string | undefined {
|
||||||
|
for (const arg of args) {
|
||||||
|
if (!arg) continue;
|
||||||
|
const flag = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
||||||
|
if (CURL_TRANSPORT_OVERRIDE_FLAGS.has(flag)) return flag;
|
||||||
|
if (arg.startsWith('-x') && arg !== '-X' && arg.length > 2) return '-x';
|
||||||
|
if (arg.startsWith('-K') && arg.length > 2) return '-K';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeCurlUrlOperand(arg: string): boolean {
|
||||||
|
return /^[a-z][a-z0-9+.-]*:\/\/\S+/i.test(arg)
|
||||||
|
|| /^(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:[/?#].*)?$/i.test(arg)
|
||||||
|
|| /^(?:localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d+)?(?:[/?#].*)?$/i.test(arg)
|
||||||
|
|| /^\[[0-9a-f:]+\](?::\d+)?(?:[/?#].*)?$/i.test(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countCurlUrlOperands(args: string[]): number {
|
||||||
|
let count = 0;
|
||||||
|
let skipNext = false;
|
||||||
|
let endOfOptions = false;
|
||||||
|
for (const arg of args) {
|
||||||
|
if (!arg) continue;
|
||||||
|
if (skipNext) { skipNext = false; continue; }
|
||||||
|
if (!endOfOptions && arg === '--') { endOfOptions = true; continue; }
|
||||||
|
if (!endOfOptions && arg.startsWith('--')) {
|
||||||
|
const hasInlineValue = arg.includes('=');
|
||||||
|
const flag = hasInlineValue ? arg.slice(0, arg.indexOf('=')) : arg;
|
||||||
|
if (!hasInlineValue && CURL_VALUE_FLAGS.has(flag)) skipNext = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!endOfOptions && arg.startsWith('-')) {
|
||||||
|
if (arg.length === 2 && CURL_VALUE_FLAGS.has(arg)) skipNext = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (looksLikeCurlUrlOperand(arg)) count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
function parseCommand(command: string): ParsedCommand | { error: string } {
|
function parseCommand(command: string): ParsedCommand | { error: string } {
|
||||||
if (SHELL_META.test(command) || COMMAND_CONTROL.test(command)) return { error: 'Shell control characters are not allowed; use direct argv-style commands only.' };
|
if (SHELL_META.test(command) || COMMAND_CONTROL.test(command)) return { error: 'Shell control characters are not allowed; use direct argv-style commands only.' };
|
||||||
@@ -355,6 +428,15 @@ function parseCommand(command: string): ParsedCommand | { error: string } {
|
|||||||
if (adapter?.execution === 'catalog_only' || adapter?.execution === 'import_only') {
|
if (adapter?.execution === 'catalog_only' || adapter?.execution === 'import_only') {
|
||||||
return { error: `Tool is catalog-only and cannot be executed directly: ${bin}` };
|
return { error: `Tool is catalog-only and cannot be executed directly: ${bin}` };
|
||||||
}
|
}
|
||||||
|
if (bin === 'curl') {
|
||||||
|
const overrideFlag = findCurlTransportOverrideFlag(args);
|
||||||
|
if (overrideFlag) {
|
||||||
|
return { error: `curl flag ${overrideFlag} changes the effective network destination and is not allowed through /api/tools/execute.` };
|
||||||
|
}
|
||||||
|
if (countCurlUrlOperands(args) > 1) {
|
||||||
|
return { error: 'curl commands with multiple URL operands are not allowed through /api/tools/execute; submit one transfer per approved target.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
return { bin, args };
|
return { bin, args };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user