The aibolit function in src/aibolit.ts interpolates the caller-supplied path straight into a /bin/bash -c "..." command and hands it to execSync. Any value containing shell metacharacters — backticks, $(...), ;, &&, or even an unbalanced double-quote — is parsed by the shell instead of treated as a filename.
The vulnerable call sits around line 18 of src/aibolit.ts:
execSync(`/bin/bash -c "set -o pipefail; (python3 -m aibolit check --full --filenames ${path} || true)"`)
The MCP tool in src/tools.ts declares the path parameter as a free-form z.string(), so the value reaches this line without validation. A model invoking the tool with a crafted string such as foo.java; touch /tmp/pwned executes both halves on the host that runs the server.
The smallest fix is to drop /bin/bash -c and call execFileSync('python3', ['-m', 'aibolit', 'check', '--full', '--filenames', path]), letting Node pass the path as one argv element so the shell never sees it. The || true swallow can move into the try/catch already present in src/safe.ts.
The
aibolitfunction insrc/aibolit.tsinterpolates the caller-suppliedpathstraight into a/bin/bash -c "..."command and hands it toexecSync. Any value containing shell metacharacters — backticks,$(...),;,&&, or even an unbalanced double-quote — is parsed by the shell instead of treated as a filename.The vulnerable call sits around line 18 of
src/aibolit.ts:The MCP tool in
src/tools.tsdeclares thepathparameter as a free-formz.string(), so the value reaches this line without validation. A model invoking the tool with a crafted string such asfoo.java; touch /tmp/pwnedexecutes both halves on the host that runs the server.The smallest fix is to drop
/bin/bash -cand callexecFileSync('python3', ['-m', 'aibolit', 'check', '--full', '--filenames', path]), letting Node pass the path as one argv element so the shell never sees it. The|| trueswallow can move into thetry/catchalready present insrc/safe.ts.