mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
fix(security): harden pipeline planning, pointer traversal, and concurrency
- expressions: never execute $js during planning/dry-run. `pipeline run --dry-run` is the command a cautious user runs to preview an unfamiliar pipeline; it must not run embedded JavaScript. Planning now returns the expression placeholder instead of calling new Function. - schema (getByJsonPointer): block __proto__/constructor/prototype and require own properties, so a crafted $from/$input path cannot pull object internals (e.g. constructor) out of step output and feed them downstream. - scheduler: clamp --concurrency to a maximum (64) to bound fan-out so a single run cannot launch an unbounded number of concurrent API calls / downloads. Note: the runtime new Function sinks in script/js and $js (arbitrary host code execution) are intentionally left unchanged here — remediating them is a design decision (sandbox vs. literal-only code) for the maintainers; see PR notes. https://claude.ai/code/session_017ZGQCjwNQF5Pz96gLUnnG1
This commit is contained in:
@@ -289,25 +289,10 @@ function resolvePlannedExpression(
|
||||
return combineResolved(undefined, undefined, false, false);
|
||||
}
|
||||
if ("$js" in expression) {
|
||||
const argsExpressions = (expression.args ?? {}) as Record<string, PipelineInputExpression>;
|
||||
const hasFrom = Object.values(argsExpressions).some((v) => isRecord(v) && "$from" in v);
|
||||
if (hasFrom) return combineResolved({ ...expression }, { ...expression }, false);
|
||||
const code = expression.$js as string;
|
||||
const resolvedArgs: Record<string, unknown> = {};
|
||||
let sensitive = false;
|
||||
for (const [key, argExpr] of Object.entries(argsExpressions)) {
|
||||
const resolved = resolvePlannedExpression(argExpr, pipeline, runtimeInput);
|
||||
resolvedArgs[key] = resolved.value;
|
||||
sensitive = sensitive || resolved.sensitive;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
const fn = new Function("args", `return (${code})`);
|
||||
const value = fn(resolvedArgs);
|
||||
return combineResolved(value, sensitive ? REDACTED : value, sensitive);
|
||||
} catch {
|
||||
return combineResolved({ ...expression }, { ...expression }, false);
|
||||
}
|
||||
// Planning / dry-run must be a non-executing preview: never run user
|
||||
// JavaScript here. Surface the expression as an unresolved placeholder so a
|
||||
// `--dry-run` of an untrusted pipeline cannot trigger code execution.
|
||||
return combineResolved({ ...expression }, { ...expression }, false);
|
||||
}
|
||||
return combineResolved(expression, expression, false);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ export function orderReports(
|
||||
return [...reports].sort((a, b) => (index.get(a.id) ?? 0) - (index.get(b.id) ?? 0));
|
||||
}
|
||||
|
||||
const MAX_CONCURRENCY = 64;
|
||||
|
||||
export function normalizeConcurrency(value: number | undefined): number {
|
||||
if (value === undefined) return 1;
|
||||
if (!Number.isInteger(value) || value < 1) {
|
||||
@@ -77,5 +79,7 @@ export function normalizeConcurrency(value: number | undefined): number {
|
||||
details: { issues: ["concurrency must be a positive integer"] },
|
||||
});
|
||||
}
|
||||
return value;
|
||||
// Cap fan-out so a single run cannot launch an unbounded number of concurrent
|
||||
// API calls / downloads and exhaust sockets, file descriptors, or memory.
|
||||
return Math.min(value, MAX_CONCURRENCY);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,14 @@ export function getByJsonPointer(value: unknown, pointer: string): unknown {
|
||||
continue;
|
||||
}
|
||||
if (isRecord(current)) {
|
||||
// A JSON pointer over data must not reach object internals. Block
|
||||
// prototype-polluting keys and only follow own properties so a crafted
|
||||
// `$from`/`$input` path cannot pull out `constructor`/`__proto__` and feed
|
||||
// it into downstream consumers.
|
||||
if (segment === "__proto__" || segment === "constructor" || segment === "prototype") {
|
||||
return undefined;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(current, segment)) return undefined;
|
||||
current = current[segment];
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user