# CVE-2026-66902 — Root Cause Analysis

## Summary

Google::Auth for Perl (CPAN distribution `Google-Auth`, maintained at
`GoogleCloudPlatform/google-auth-library-perl`) executes a command taken verbatim
from an external_account credentials JSON file through a single-argument
`system($command)` call. In versions before 0.06 there is no opt-in gate: any
application that builds Application Default Credentials from a configuration it
does not fully control runs the embedded shell command (with full `/bin/sh -c`
interpretation and attacker-chosen environment variables) with the privileges of
the application process. This is CWE-78 OS command injection leading to arbitrary
OS command execution.

## Impact

- **Package/component:** CPAN `Google-Auth` (`Google::Auth`),
  `lib/Google/Auth/ExternalAccountCredentials/Pluggable.pm`
- **Affected versions:** < 0.06 (verified at commit
  `913fb1780202c1ee9dd640c28c01549903f8e23a` = fix commit parent; packaged as 0.05)
- **Risk level:** Critical. Any service/workload that consumes a credentials JSON
  from an untrusted or partially trusted source (mounted config, user-supplied
  file, CI artifact) executes attacker-chosen shell commands as the application
  user.

## Impact Parity

- **Disclosed/claimed maximum impact:** arbitrary OS command execution (RCE) in the
  application process.
- **Reproduced impact from this run:** arbitrary shell command execution in the
  Perl application process. The embedded command used shell output redirection to
  write a unique attacker-chosen marker file, and consumed attacker-controlled
  environment variables copied from the same JSON (`environment_variables` map) —
  proving both full shell interpretation and environment injection.
- **Parity:** `full`.
- **Not demonstrated:** nothing material; the claimed impact (code/command
  execution) was reproduced directly, twice, against the real library entrypoint.

## Root Cause

`Google::Auth::ExternalAccountCredentials::Pluggable::retrieve_subject_token()`
(in versions < 0.06) does the following with zero validation and no opt-in:

1. Copies every entry of `credential_source.executable.environment_variables`
   from the credentials JSON into `%ENV`.
2. Reads `credential_source.executable.command` and runs
   `capture { system($command) }`. Because `system()` receives a single string,
   Perl invokes `/bin/sh -c`, giving the attacker pipes, redirection, command
   substitution, and all shell metacharacters.

Dispatch reaches this subclass automatically: `Google::Auth->default()` →
`Google::Auth::DefaultCredentials->from_env()` reads the JSON named by
`GOOGLE_APPLICATION_CREDENTIALS` → `make_creds()` in
`ExternalAccountCredentials.pm` selects the `Pluggable` subclass whenever
`credential_source.executable` exists → construction succeeds with only
`audience`, `subject_token_type`, `token_url`, and `credential_source` → the
first `fetch_access_token()` (which every consuming application performs to use
the credential) calls `retrieve_subject_token()` and executes the command before
any network access.

Fix commit `c95c77e70bec94f17e239d88050f843ea1cade95` (released as 0.06) adds an
opt-in gate at the top of `retrieve_subject_token()` that throws unless
`GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1`, plus a bounded execution timeout,
schema validation of the command output, and URL domain validation in the base
class. Version 0.10 additionally parses with `Text::ParseWords` and uses indirect
`system` exec.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` (self-contained; re-runnable).
2. The script:
   - Clones `GoogleCloudPlatform/google-auth-library-perl` (into the prepared
     project cache when available) and resolves the vulnerable checkout as
     `c95c77e70bec94f17e239d88050f843ea1cade95^` (= `913fb17`) and the fixed
     checkout as the fix commit itself; verifies the gate string is absent in the
     vulnerable tree and present in the fixed tree.
   - Installs the pure-Perl runtime dependencies (Moo, Capture::Tiny,
     LWP::UserAgent, Log::Any, Throwable, URI) via apt when permitted, otherwise
     into a bundle-local `INSTALL_BASE` with `cpan`, then builds the real module
     including its XS component (`perl Makefile.PL && make`) for both versions.
   - Generates an attacker-controlled `external_account` credentials JSON whose
     `credential_source.executable.command` writes a unique marker file using
     shell redirection and attacker-injected environment variables, and points
     `token_url` at a closed localhost port so the STS exchange fails fast after
     the command has already executed.
   - Invokes the real ADC flow as a CLI command:
     `GOOGLE_APPLICATION_CREDENTIALS=<json> perl -I... trigger.pl`, where
     `trigger.pl` calls `Google::Auth->default()` and then
     `fetch_access_token()` — exactly the sequence a real application performs.
   - Runs the matrix: 2 vulnerable attempts, 2 fixed attempts (default gated),
     and 1 fixed attempt with the opt-in gate set (positive control).
3. Expected evidence: both vulnerable attempts create their unique marker files;
   both fixed attempts throw
   `Pluggable credentials are not enabled. Set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1`
   and create no marker; the opt-in control creates its marker again.

## Evidence

- Main log: `bundle/logs/reproduction_steps.log`
- Per-attempt process logs: `bundle/logs/attempt_vuln_1.log`,
  `bundle/logs/attempt_vuln_2.log`, `bundle/logs/attempt_fixed_1.log`,
  `bundle/logs/attempt_fixed_2.log`, `bundle/logs/attempt_fixed_allow1.log`
- Marker files (written by the injected shell command through `/bin/sh -c`):
  `bundle/repro/markers/vuln_1.marker`,
  `bundle/repro/markers/vuln_2.marker`,
  `bundle/repro/markers/fixed_allow1.marker`
- Per-attempt observation JSONs: `bundle/repro/observations/*.json`
- Attacker configs used: `bundle/repro/adc/*.json`
- Runtime manifest: `bundle/repro/runtime_manifest.json`

Key excerpts (identical across two consecutive runs):

```
[run] vuln attempt 1 ... 
RESULT: credentials class = Google::Auth::ExternalAccountCredentials::Pluggable
RESULT: fetch_access_token error: Token exchange failed with status 500: Can't connect to 127.0.0.1:9
[run] vuln attempt 1: MARKER CREATED -> pwned-via-CVE-2026-66902 vuln attempt 1 shell+env injection
[matrix] vulnerable attempts with marker: 2/2

RESULT: fetch_access_token error: Pluggable credentials are not enabled. Set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1 to enable.
[matrix] fixed attempts blocked (no marker): 2/2

[run] fixed attempt allow1: MARKER CREATED -> pwned-via-CVE-2026-66902 fixed attempt allow1 shell+env injection
[matrix] fixed+opt-in attempts with marker: 1/1
=== RESULT: CONFIRMED - command injection via Pluggable external_account credentials ===
```

Environment: Ubuntu 24.04, perl 5.38.2 (x86_64-linux-gnu-thread-multi),
OpenSSL 3.0.13 headers for the XS build, pure-Perl deps installed into
`bundle/repro/deps` (cpan `INSTALL_BASE`). No sanitizers, no mocks, no network
interaction with Google endpoints (token_url is `http://127.0.0.1:9/v1/token`,
which fails *after* the injected command has executed).

## Recommendations / Next Steps

- Upgrade to Google-Auth 0.06 or later; 0.06 throws unless
  `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1` is set, and 0.10 additionally
  shell-parses the command with `Text::ParseWords` and uses indirect (list-form)
  `system` exec, removing `/bin/sh -c` interpretation.
- Treat every credentials JSON reachable by the ADC flow
  (`GOOGLE_APPLICATION_CREDENTIALS`, well-known paths) as executable code: restrict
  write access, prefer trusted provisioning, and avoid setting the opt-in gate.
- Regression testing: the upstream fix commit already adds tests
  (`t/16-pluggable-credentials.t`); downstream should additionally test that a
  `credential_source.executable` config without the gate never spawns a process.

## Additional Notes

- **Idempotency:** the script was executed twice consecutively (plus two more
  times after adding unique per-attempt markers/observations) — every run
  produced the full matrix result (2/2 vulnerable markers, 2/2 fixed blocked,
  1/1 opt-in control) and exit code 0.
- The command executes *before* the STS token exchange; the proof intentionally
  uses a closed-loopback `token_url` so no external network call is needed and
  the marker is created regardless of the later (expected) STS failure.
- Both the claimed entrypoint variants are supported: the primary proof uses
  `Google::Auth->default()` (requires the XS build, which the script performs);
  if the XS toolchain were unavailable the script falls back to
  `Google::Auth::DefaultCredentials->from_env()`, which is the same dispatch
  path named in the claim.
- Edge case: the vulnerable code only executes the command when a token is
  fetched, not at config parse time — matching real application behavior, since
  any consumer of the credential calls `fetch_access_token()` to use it.
