diff --git a/packages/cli/src/pipeline/expressions.ts b/packages/cli/src/pipeline/expressions.ts index ef11cf1..4e313af 100644 --- a/packages/cli/src/pipeline/expressions.ts +++ b/packages/cli/src/pipeline/expressions.ts @@ -289,25 +289,10 @@ function resolvePlannedExpression( return combineResolved(undefined, undefined, false, false); } if ("$js" in expression) { - const argsExpressions = (expression.args ?? {}) as Record; - 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 = {}; - 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); } diff --git a/packages/cli/src/pipeline/scheduler.ts b/packages/cli/src/pipeline/scheduler.ts index 8a7ab7a..5a2458c 100644 --- a/packages/cli/src/pipeline/scheduler.ts +++ b/packages/cli/src/pipeline/scheduler.ts @@ -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); } diff --git a/packages/cli/src/pipeline/schema.ts b/packages/cli/src/pipeline/schema.ts index 744d826..6ad4b2b 100644 --- a/packages/cli/src/pipeline/schema.ts +++ b/packages/cli/src/pipeline/schema.ts @@ -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; }