mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* Widen Windows shim ratchet to detect package bin spawns Follow local program expressions into node_modules/.bin while preserving the existing literal check, roots, and allow-list. Document static-analysis limits and cover unsafe and resolver-based invocations. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(scripts): fold dot segments before matching node_modules/.bin The predicate joins call arguments textually, so a literal '..' segment hid a path that resolves into node_modules/.bin at runtime. Folds '.' and '..' (and Windows separators) first. A '..' that genuinely escapes .bin still does not match. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(scripts): use .at(-1) in the dot-segment fold oxlint's prefer-at rule; the repo-wide lint gate is an error, not a warning. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
101 lines
3.1 KiB
JavaScript
101 lines
3.1 KiB
JavaScript
import ts from 'typescript-api'
|
|
|
|
const SPAWN_METHODS = new Set(['spawn', 'spawnSync', 'execFile', 'execFileSync'])
|
|
const BIN_PATH = /(?:^|[\\/])node_modules[\\/]\.bin(?:[\\/]|$)/i
|
|
|
|
export function hasNodeModulesBinSpawn(contents) {
|
|
const source = ts.createSourceFile(
|
|
'script.mjs',
|
|
contents,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
)
|
|
const bindings = new Map()
|
|
const calls = []
|
|
function visit(node) {
|
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
const initializers = bindings.get(node.name.text) ?? []
|
|
initializers.push(node.initializer)
|
|
bindings.set(node.name.text, initializers)
|
|
}
|
|
if (ts.isCallExpression(node)) {
|
|
calls.push(node)
|
|
}
|
|
ts.forEachChild(node, visit)
|
|
}
|
|
visit(source)
|
|
|
|
function name(node) {
|
|
return ts.isIdentifier(node)
|
|
? node.text
|
|
: ts.isPropertyAccessExpression(node)
|
|
? node.name.text
|
|
: ''
|
|
}
|
|
|
|
function paths(node, seen = new Set()) {
|
|
if (!node || seen.has(node)) {
|
|
return ['?']
|
|
}
|
|
const next = new Set(seen).add(node)
|
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
return [node.text]
|
|
}
|
|
if (ts.isIdentifier(node)) {
|
|
return (bindings.get(node.text) ?? []).flatMap((value) => paths(value, next))
|
|
}
|
|
if (ts.isParenthesizedExpression(node)) {
|
|
return paths(node.expression, next)
|
|
}
|
|
if (ts.isConditionalExpression(node)) {
|
|
return [...paths(node.whenTrue, next), ...paths(node.whenFalse, next)]
|
|
}
|
|
if (ts.isTemplateExpression(node)) {
|
|
return node.templateSpans.reduce(
|
|
(prefixes, span) => combine(prefixes, paths(span.expression, next), '', span.literal.text),
|
|
[node.head.text]
|
|
)
|
|
}
|
|
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
return combine(paths(node.left, next), paths(node.right, next))
|
|
}
|
|
if (ts.isCallExpression(node) && ['join', 'resolve'].includes(name(node.expression))) {
|
|
return node.arguments.reduce(
|
|
(prefixes, arg) => combine(prefixes, paths(arg, next), '/'),
|
|
['']
|
|
)
|
|
}
|
|
return ['?']
|
|
}
|
|
|
|
function combine(left, right, separator = '', suffix = '') {
|
|
return (left.length ? left : ['?']).flatMap((prefix) =>
|
|
(right.length ? right : ['?']).map((part) => `${prefix}${separator}${part}${suffix}`)
|
|
)
|
|
}
|
|
|
|
// Why: arguments are folded textually, so a literal '..' segment would otherwise hide a
|
|
// path that resolves into node_modules/.bin at runtime.
|
|
function foldDotSegments(value) {
|
|
const folded = []
|
|
for (const segment of value.replace(/\\/g, '/').split('/')) {
|
|
if (segment === '.' || segment === '') {
|
|
continue
|
|
}
|
|
if (segment === '..' && folded.length && folded.at(-1) !== '..') {
|
|
folded.pop()
|
|
continue
|
|
}
|
|
folded.push(segment)
|
|
}
|
|
return folded.join('/')
|
|
}
|
|
|
|
return calls.some(
|
|
(call) =>
|
|
SPAWN_METHODS.has(name(call.expression)) &&
|
|
paths(call.arguments[0]).some((value) => BIN_PATH.test(foldDotSegments(value)))
|
|
)
|
|
}
|