← All MCP servers
MCP server

io.github.webdriverio/mcp

mcp:webdriverio/mcp

MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)

Verdict
Suspicious

Risky behavior the analysis could not rule out.

Analyzed Oct 26, 2026, 12:27 UTC at 8ffc83c
2
findings
medium
highest severity
2
threat classes
184
files scanned
1,870/1,870
checks satisfied
0
package advisories

Why this verdict

  • suspicious: finding mcp-server:.:src/providers/local-browser.provider.ts:132:IMPERSONATE
  • suspicious: finding mcp-server:.:src/providers/local-browser.provider.ts:40:PRIVESC

Security findings 2

Impersonation

mediumContextual risklow confidence

The provider rewrites the headless browser's User-Agent to look like a headed Chrome, and hides automation signals, in order to avoid bot detection by sites.

await browser.emulationSetUserAgentOverride({ userAgent: current.replace(HEADLESS_TOKEN, 'Chrome/') });
evasion:scannervector:code
Counter-argument considered

Rewriting the headless UA token is an ordinary browser-automation capability, done to avoid sites blocking HeadlessChrome. It involves no credential, exfiltration, or agent-directed deception.

Privilege escalation

mediumContextual risklow confidence

The local browser provider launches Chromium with the sandbox and web security disabled and insecure content allowed, which weakens isolation when the agent browses untrusted pages.

'--disable-web-security',
vector:code
Counter-argument considered

The default Chromium args unconditionally disable the sandbox and web security and are not declared. This is a real isolation weakening for a browser that may visit untrusted pages, so it is not refuted, though severity is low.

Open questions 4

What the analysis could not settle from the code alone.

[file request src/tools] Tool descriptions and input schemas are registered dynamically from definition objects; do any descriptions or parameter descriptions steer the agent beyond the declared automation task, or condition other tools?
Tool poisoning
[file request src/resources] Resource definitions are registered dynamically; do resource names or descriptions contain agent-directed instructions?
Tool poisoning
[file request src/resources/testmu-local.resource.ts] Where does getLocalBinaryInfo() get downloadUrl (official LambdaTest/TestMu domain or mutable/other host)? The resource tells the agent to curl, unzip and run that binary with the access key.
Code execution
[file request src/utils/docs-client.ts] Where does loadDocsIndex fetch the docs index from, is the URL mutable or remote, and is the fetched content passed to the agent or executed?
Supply chain
Refuted findings 13

Candidates the review stage examined and rejected, kept for transparency.

Credential access

criticalRefutedlow confidence

The BrowserStack local-binary resource reads BROWSERSTACK_ACCESS_KEY from the environment and embeds the real key in resource text (setup commands) returned to the LLM/client, exposing the secret in agent context, while the resource description tells the agent it MUST follow these instructions.

const accessKey = process.env.BROWSERSTACK_ACCESS_KEY ?? '<BROWSERSTACK_ACCESS_KEY>';
Why it was refuted

The env key is for the wrapped BrowserStack service and is placed into that vendor's own tunnel setup commands. That falls under the env-API-key-for-the-wrapped-service carve-out, and nothing is sent to a third party.

Credential access

criticalRefutedlow confidence

The Sauce Connect resource reads SAUCE_ACCESS_KEY from the environment and places the actual key into command lines returned in resource content to the model, leaking the secret into agent context (the key is for the wrapped service).

const accessKey = process.env.SAUCE_ACCESS_KEY ?? '<SAUCE_ACCESS_KEY>';
Why it was refuted

The Sauce key is the wrapped service's own env credential, used to build Sauce Connect setup commands. This is the wrapped-service env-key carve-out, with no third-party sink.

Credential access

criticalRefutedlow confidence

The TestingBot tunnel resource reads TESTINGBOT_KEY/SECRET from the environment and embeds them in setup commands returned to the model, exposing the secrets in agent context (vendor-specific, plausibly honest convenience).

const key = process.env.TESTINGBOT_KEY ?? '<TESTINGBOT_KEY>';
Why it was refuted

The TestingBot key is the wrapped service's own env credential, used to build the vendor tunnel launch command. This is the env-key carve-out.

Credential access

criticalRefutedlow confidence

A resource whose description says it MUST be read and followed reads TESTINGBOT_KEY and TESTINGBOT_SECRET from the environment and embeds them in command lines returned to the agent, so the secrets enter the model context and any logs.

java -jar ${JAR_NAME} ${key} ${secret}
Why it was refuted

The 'MUST be read' wording only orders tunnel startup before start_session. The key and secret belong to the wrapped TestingBot service and go into its own tunnel command, which is the env-key carve-out.

Credential access

criticalRefutedlow confidence

The testmu-local-binary resource reads the TestMu access key from the environment and embeds it in plaintext in resource content and shell commands handed to the agent, which exposes the secret to the LLM context and transcript; this is undeclared and goes beyond what the tunnel instructions need.

const accessKey = process.env.TESTMU_ACCESS_KEY ?? '<TESTMU_ACCESS_KEY>';
Why it was refuted

The TestMu key is the wrapped service's own env credential, used in that vendor's tunnel setup commands. This is the env-key carve-out, with no third-party sink.

Credential access

criticalRefutedlow confidence

A resource whose description says it MUST be read and followed reads TESTMU_ACCESS_KEY from the environment and embeds it in daemon start commands returned to the agent, so the secret enters the model context and any logs.

4. Start daemon: ./${info.binaryName} --user ${username} --key ${accessKey}
Why it was refuted

The 'MUST be read and followed' text only orders tunnel startup before start_session. The key is the wrapped TestMu service's own credential, used in its daemon command, which is the env-key carve-out.

Data exfiltration

criticalRefutedlow confidence

The wdio-show-trace CLI reads a user-specified or latest trace zip and serves it on a localhost port, then opens the third-party player.vibium.dev viewer with the local URL; the third-party origin is granted CORS access to fetch the trace file contents (which may contain screenshots and session data).

const viewerUrl = `https://player.vibium.dev/?record=${encodeURIComponent(traceUrl)}`;
Why it was refuted

This is a user-run trace-viewer CLI. Serving the trace on 127.0.0.1 and opening the Vibium player is the tool's purpose, so it is a declared core capability rather than a covert exfil channel.

Data exfiltration

criticalRefutedlow confidence

The BrowserStack upload_app tool streams any agent-supplied path to BrowserStack with no .apk/.ipa validation, so it could upload arbitrary local files off-host; the tool does not declare this.

const stream = createReadStream(path);
Why it was refuted

Uploading a local file to BrowserStack App Automate is the tool's declared core function. The destination is the fixed vendor API using the user's own credentials, so it is not an exfil channel.

Data exfiltration

criticalRefutedlow confidence

The cloud-provider upload_app tool reads any agent-supplied path into memory and uploads it to a third-party cloud with no extension or content check, even though the tool claims to upload only .apk/.ipa files.

const fileBuffer = readFileSync(path);
Why it was refuted

The upload_app tool declares that it uploads a local app file to a named cloud provider. The sink is the provider's fixed API with the user's own auth, which is the declared core capability and not covert exfiltration.

Code execution

criticalRefutedmedium confidence

execute_electron_script runs agent-supplied JavaScript in the Electron main process via new Function. This is openly declared as privileged arbitrary execution, but it is a powerful capability.

new Function('electron', 'args', source)(electron, values)
Why it was refuted

The tool openly declares arbitrary JavaScript execution in the Electron main process, marks it destructive, and warns to use trusted scripts only. This is its declared core capability.

Credential access

criticalRefutedmedium confidence

The launch_chrome tool with copyProfileFiles=true copies the user's real Chrome profile (Local State and Default dir with cookies/logins) into a temp debug dir with remote debugging enabled; this is disclosed in the tool description and opt-in, but gives the agent access to logged-in sessions.

cpSync(join(srcDir, 'Default'), join(USER_DATA_DIR, 'Default'), { recursive: true })
Why it was refuted

The profile copy is opt-in (default false), declared in the tool description, and goes to a local temp dir for attaching to a logged-in session. That is the declared core capability, with no outbound sink.

Supply chain

highRefutedlow confidence

The docs tool fetches a mutable remote corpus from webdriver.io over the network, caches it in ~/.wdio-mcp, and serves excerpts to the agent as documentation, so a compromised upstream could inject instructions; this looks like an honest docs-search feature.

export const DOCS_URL = 'https://webdriver.io/llms-full.txt';
Why it was refuted

Fetching the official WebdriverIO docs corpus for a docs-search feature is the tool's core purpose. It is a validated fetch with a marker check, it is cached locally, and there is no instruction or execution path.

Supply chain

mediumRefutedlow confidence

package.json declares an undeclared 'prepare: husky' lifecycle script that runs on install/pack in the dev repo; it is a standard git-hooks dev tool and not a consumer-facing install hook, so risk is low.

"prepare": "husky"
Why it was refuted

The husky prepare script is a standard git-hooks dev tool that runs only in the dev repo; the published package ships only lib and README, and it contains no payload.

Coverage gaps 11

Parts of the artifact the static analysis could not fully examine.

dynamic_metadata line 124: tool name is computed: definition.name
src/server.ts
dynamic_metadata line 124: tool None description is computed: definition.description
src/server.ts
dynamic_metadata line 124: tool None input schema is computed: definition.inputSchema
src/server.ts
dynamic_metadata line 135: resource name is computed: definition.name
src/server.ts
dynamic_metadata line 135: resource None description is computed: definition.description
src/server.ts
dynamic_metadata line 142: resource name is computed: definition.name
src/server.ts
dynamic_metadata line 142: resource None description is computed: definition.description
src/server.ts
no_dataflow_for_dynamic_code line 29: invoked code is not structurally extracted
src/tools/electron-execute.tool.ts
no_dataflow_for_dynamic_code line 13: invoked code is not structurally extracted
src/tools/execute-script.tool.ts
request_unanswered request.0: declared-vs-actual.9/testmu-dl: no answer
request_unanswered request.0: behavior-chains.5/docs-client: no answer