@wdio/browserstack-service: OS command injection via crafted git branch name
What's the vulnerability?
@wdio/browserstack-service is the BrowserStack integration for WebdriverIO,
shipped from the webdriverio monorepo (package source under
packages/wdio-browserstack-service/). To support BrowserStack's AI test
selection feature it collects git metadata about the working directory.
The helper getGitMetadataForAISelection() interpolates the current git
branch name directly into a shell command string that is passed to
child_process.execSync(). The branch name is never sanitized or escaped.
Because a git branch name can legally contain shell metacharacters
(backticks, $(...), ;, |, &), a repository whose checked-out branch
embeds a payload causes that payload to be executed by the shell the moment
the service runs the helper. This is arbitrary OS command execution in the
context of the process running the WebdriverIO test session — typically a CI
runner — i.e. remote code execution (CWE-78).
This is realistic in CI: a pull-request branch name is attacker-controlled, and automation that checks out the PR branch and runs the BrowserStack service will execute whatever the attacker put in the branch name.
Root Cause Analysis
# RCA Report — CVE-2026-25244
## Summary
CVE-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.
## Impact
- **Package**: `@wdio/browserstack-service` (webdriverio monorepo)
- **Affected versions**: `<= 9.23.2`
- **Fixed version**: `9.24.0`
- **Risk level**: Critical (CVSS 9.8)
- **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.
## Root Cause
The vulnerable code in `getGitMetadataForAISelection()` (v9.23.2) uses `child_process.execSync()` with JavaScript template literals to build git commands:
```typescript
const changedFilesOutput = execSync(`git diff --name-only ${baseBranch}..${currentBranch}`).toString().trim()
const commitsOutput = execSync(`git log ${baseBranch}..${currentBranch} --pretty=%H`).toString().trim()
```
When `currentBranch` (or `baseBranch` / `commit`) contains shell metacharacters, `execSync()` passes the entire string to `/bin/sh`, which interprets the metacharacters and executes the embedded commands.
The 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.
## Reproduction Steps
The reproduction script is `repro/reproduction_steps.sh`. It performs the following:
1. Clones the webdriverio repository and extracts `helpers.ts` from tags `v9.23.2` (vulnerable) and `v9.24.0` (fixed).
2. Creates a mock `@wdio/logger` module so the TypeScript source compiles with `tsx`.
3. Creates a dummy git repository for the test.
4. Creates a fake `git` binary that returns a malicious branch name (`master; touch <marker>`) when asked for the current branch.
5. 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.
6. 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.
7. Prints a summary of code differences and test results.
**Expected evidence**:
- Vulnerable run: marker file **created** (`VULNERABLE: marker file was created by injected command!`)
- Fixed run: marker file **NOT created** (`FIXED: marker file was NOT created.`)
## Evidence
**Log files**:
- `logs/vulnerable_output.log` — output of running the vulnerable `getGitMetadataForAISelection()`
- `logs/fixed_output.log` — output of running the fixed `getGitMetadataForAISelection()`
**Key excerpts from vulnerable run** (`logs/vulnerable_output.log`):
```
VULNERABLE: marker file was created by injected command!
The shell interpreted metacharacters in the branch name
and executed the embedded touch command.
```
**Key excerpts from fixed run** (`logs/fixed_output.log`):
```
[WARN] Invalid current branch name detected: master; touch /tmp/.../marker_fixed. Skipping this folder for security reasons.
Result: []
FIXED: marker file was NOT created.
The sanitization (isValidGitRef + spawnSync) prevented
shell metacharacters from being evaluated.
```
**Code diff highlights** (from `repro/reproduction_steps.sh` output):
```
Vulnerable (v9.23.2) - uses execSync with string interpolation:
204: execSync(`git diff --name-only ${baseBranch}..${currentBranch}`)
209: execSync(`git log ${baseBranch}..${currentBranch} --pretty=%H`)
Fixed (v9.24.0) - uses spawnSync with array arguments:
3: import { spawnSync } from 'node:child_process'
29: spawnSync('git', args, { ... })
Fixed (v9.24.0) - adds isValidGitRef validation:
15: const SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9_./-]+$/
17: function isValidGitRef(ref: string): boolean { ... }
```
## Recommendations / Next Steps
1. **Upgrade immediately** to `@wdio/browserstack-service >= 9.24.0` (or webdriverio >= 9.24.0).
2. **Defense in depth**: CI/CD pipelines should validate branch names before checking them out, especially for fork/PR builds.
3. **Code review**: Audit any other `execSync()` usages in the webdriverio monorepo that interpolate user-controlled data into shell commands.
4. **Regression testing**: Add unit tests that pass malicious branch names and commit hashes to `getGitMetadataForAISelection()` to ensure the validation regex catches future bypasses.
## Additional Notes
- **Idempotency**: The reproduction script was run **twice consecutively** and produced identical results both times.
- **No live BrowserStack account needed**: The vulnerability is entirely in local git-metadata collection. The reproduction uses only Node.js, git, and `tsx`.
- **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.
- **Fix commit**: https://github.com/webdriverio/webdriverio/commit/0e6748ecdb116f80495449a758d430201106dbcc
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00143 pruva-verify GHSA-5c46-x3qw-q7j7 pruva-verify CVE-2026-25244 curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00143/artifacts/reproduction_steps.sh Make executable
chmod +x reproduction_steps.sh Run the script
./reproduction_steps.sh How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available