{"repro_id":"REPRO-2026-00143","version":8,"title":"@wdio/browserstack-service: OS command injection via crafted git branch name","repro_type":"security","status":"published","severity":"critical","description":"`@wdio/browserstack-service` is the BrowserStack integration for WebdriverIO,\nshipped from the `webdriverio` monorepo (package source under\n`packages/wdio-browserstack-service/`). To support BrowserStack's AI test\nselection feature it collects git metadata about the working directory.\n\nThe helper `getGitMetadataForAISelection()` interpolates the **current git\nbranch name** directly into a shell command string that is passed to\n`child_process.execSync()`. The branch name is never sanitized or escaped.\n\nBecause a git branch name can legally contain shell metacharacters\n(backticks, `$(...)`, `;`, `|`, `&`), a repository whose checked-out branch\nembeds a payload causes that payload to be **executed by the shell** the moment\nthe service runs the helper. This is arbitrary OS command execution in the\ncontext of the process running the WebdriverIO test session — typically a CI\nrunner — i.e. remote code execution (CWE-78).\n\nThis is realistic in CI: a pull-request branch name is attacker-controlled, and\nautomation that checks out the PR branch and runs the BrowserStack service will\nexecute whatever the attacker put in the branch name.","root_cause":"# RCA Report — CVE-2026-25244\n\n## Summary\n\nCVE-2026-25244 is an OS command injection vulnerability (CWE-78) in `@wdio/browserstack-service` versions `<= 9.23.2`. The `getGitMetadataForAISelection()` function in `packages/wdio-browserstack-service/src/testorchestration/helpers.ts` interpolates git branch names and commit hashes directly into shell command strings passed to `child_process.execSync()`. Because git branch names can contain shell metacharacters (backticks, `$()`, `;`, `|`, `&`), an attacker who controls a branch name (e.g., via a malicious pull request branch in CI/CD) can cause arbitrary OS command execution when the BrowserStack service runs the metadata helper.\n\n## Impact\n\n- **Package**: `@wdio/browserstack-service` (webdriverio monorepo)\n- **Affected versions**: `<= 9.23.2`\n- **Fixed version**: `9.24.0`\n- **Risk level**: Critical (CVSS 9.8)\n- **Consequences**: Remote Code Execution (RCE) in CI/CD runners. Any pipeline that checks out an attacker-controlled branch and runs WebdriverIO with the BrowserStack service can be compromised.\n\n## Root Cause\n\nThe vulnerable code in `getGitMetadataForAISelection()` (v9.23.2) uses `child_process.execSync()` with JavaScript template literals to build git commands:\n\n```typescript\nconst changedFilesOutput = execSync(`git diff --name-only ${baseBranch}..${currentBranch}`).toString().trim()\nconst commitsOutput = execSync(`git log ${baseBranch}..${currentBranch} --pretty=%H`).toString().trim()\n```\n\nWhen `currentBranch` (or `baseBranch` / `commit`) contains shell metacharacters, `execSync()` passes the entire string to `/bin/sh`, which interprets the metacharacters and executes the embedded commands.\n\nThe fix (commit `0e6748ecd`, v9.24.0) replaces `execSync()` with `spawnSync('git', [args...])`, which passes arguments directly to the `git` executable without shell interpretation. It also adds `isValidGitRef()` validation using a `SAFE_GIT_REF_PATTERN` regex (`^[a-zA-Z0-9_./-]+$`) to reject any ref containing dangerous characters before it reaches the command execution layer.\n\n## Reproduction Steps\n\nThe reproduction script is `repro/reproduction_steps.sh`. It performs the following:\n\n1. Clones the webdriverio repository and extracts `helpers.ts` from tags `v9.23.2` (vulnerable) and `v9.24.0` (fixed).\n2. Creates a mock `@wdio/logger` module so the TypeScript source compiles with `tsx`.\n3. Creates a dummy git repository for the test.\n4. Creates a fake `git` binary that returns a malicious branch name (`master; touch <marker>`) when asked for the current branch.\n5. Runs `getGitMetadataForAISelection()` from the **vulnerable** source with the fake git in `PATH`. The shell interprets the `;` in the branch name and executes the injected `touch` command, creating the marker file.\n6. Runs `getGitMetadataForAISelection()` from the **fixed** source with the same fake git. The `isValidGitRef()` check rejects the malicious branch name and the function skips the folder, leaving the marker file absent.\n7. Prints a summary of code differences and test results.\n\n**Expected evidence**:\n- Vulnerable run: marker file **created** (`VULNERABLE: marker file was created by injected command!`)\n- Fixed run: marker file **NOT created** (`FIXED: marker file was NOT created.`)\n\n## Evidence\n\n**Log files**:\n- `logs/vulnerable_output.log` — output of running the vulnerable `getGitMetadataForAISelection()`\n- `logs/fixed_output.log` — output of running the fixed `getGitMetadataForAISelection()`\n\n**Key excerpts from vulnerable run** (`logs/vulnerable_output.log`):\n```\nVULNERABLE: marker file was created by injected command!\n  The shell interpreted metacharacters in the branch name\n  and executed the embedded touch command.\n```\n\n**Key excerpts from fixed run** (`logs/fixed_output.log`):\n```\n[WARN] Invalid current branch name detected: master; touch /tmp/.../marker_fixed. Skipping this folder for security reasons.\nResult: []\n\nFIXED: marker file was NOT created.\n  The sanitization (isValidGitRef + spawnSync) prevented\n  shell metacharacters from being evaluated.\n```\n\n**Code diff highlights** (from `repro/reproduction_steps.sh` output):\n```\nVulnerable (v9.23.2) - uses execSync with string interpolation:\n204:  execSync(`git diff --name-only ${baseBranch}..${currentBranch}`)\n209:  execSync(`git log ${baseBranch}..${currentBranch} --pretty=%H`)\n\nFixed (v9.24.0) - uses spawnSync with array arguments:\n3:    import { spawnSync } from 'node:child_process'\n29:   spawnSync('git', args, { ... })\n\nFixed (v9.24.0) - adds isValidGitRef validation:\n15:   const SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9_./-]+$/\n17:   function isValidGitRef(ref: string): boolean { ... }\n```\n\n## Recommendations / Next Steps\n\n1. **Upgrade immediately** to `@wdio/browserstack-service >= 9.24.0` (or webdriverio >= 9.24.0).\n2. **Defense in depth**: CI/CD pipelines should validate branch names before checking them out, especially for fork/PR builds.\n3. **Code review**: Audit any other `execSync()` usages in the webdriverio monorepo that interpolate user-controlled data into shell commands.\n4. **Regression testing**: Add unit tests that pass malicious branch names and commit hashes to `getGitMetadataForAISelection()` to ensure the validation regex catches future bypasses.\n\n## Additional Notes\n\n- **Idempotency**: The reproduction script was run **twice consecutively** and produced identical results both times.\n- **No live BrowserStack account needed**: The vulnerability is entirely in local git-metadata collection. The reproduction uses only Node.js, git, and `tsx`.\n- **Edge case**: The `isValidGitRef()` regex (`^[a-zA-Z0-9_./-]+$`) is restrictive but safe. It may reject unusual but legitimate branch names in rare cases (e.g., names containing `@` or `#`), but this is an acceptable trade-off for preventing command injection.\n- **Fix commit**: https://github.com/webdriverio/webdriverio/commit/0e6748ecdb116f80495449a758d430201106dbcc\n","ghsa_id":"GHSA-5c46-x3qw-q7j7","cve_id":"CVE-2026-25244","cwe_id":"CWE-78 (OS Command Injection)","package":{"name":"@wdio/browserstack-service","ecosystem":"npm","affected_versions":"<= 9.23.2","fixed_version":"9.24.0","tested_patched":"9.24.0"},"reproduced_at":"2026-05-22T10:46:32.126813+00:00","duration_secs":3589.9910945892334,"tool_calls":274,"turns":234,"handoffs":3,"total_cost_usd":2.7504708600000014,"agent_costs":{"repro":1.3139428,"support":0.03828594,"vuln_variant":1.3982421199999997},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p5":0.32192629999999994,"accounts/fireworks/models/kimi-k2p6":0.9920165},"support":{"accounts/fireworks/models/kimi-k2p6":0.03828594},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":1.3982421199999997}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-22T10:46:34.147222+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":5910,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":9513,"category":"reproduction_script"},{"path":"vuln_variant/rca_report.md","filename":"rca_report.md","size":7819,"category":"analysis"},{"path":"vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":13401,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":4028,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":642,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":4820,"category":"ticket"},{"path":"repro/validation_verdict.json","filename":"validation_verdict.json","size":2699,"category":"other"},{"path":"vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":6243,"category":"documentation"},{"path":"vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3224,"category":"other"},{"path":"vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2061,"category":"other"},{"path":"logs/fixed_output.log","filename":"fixed_output.log","size":468,"category":"log"},{"path":"logs/vuln_variant/variant2_vulnerable.log","filename":"variant2_vulnerable.log","size":1162,"category":"log"},{"path":"logs/vuln_variant/variant1_vulnerable.log","filename":"variant1_vulnerable.log","size":756,"category":"log"},{"path":"logs/vuln_variant/variant2_fixed.log","filename":"variant2_fixed.log","size":311,"category":"log"},{"path":"logs/vuln_variant/variant3_vulnerable.log","filename":"variant3_vulnerable.log","size":1271,"category":"log"},{"path":"logs/vuln_variant/variant1_fixed.log","filename":"variant1_fixed.log","size":300,"category":"log"},{"path":"logs/vuln_variant/variant3_fixed.log","filename":"variant3_fixed.log","size":274,"category":"log"},{"path":"logs/vulnerable_output.log","filename":"vulnerable_output.log","size":1327,"category":"log"}]}