# REPRO-2026-00323: Google::Auth for Perl command injection: external_account credentials JSON executable run via ungated system() → RCE ## Summary Status: published Severity: critical CVSS: Unknown CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00323 CVE: CVE-2026-66902 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # 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= 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. ## Reproduction Details Reproduced: 2026-08-23T15:38:13.515Z Duration: 1012 seconds Tool calls: 169 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00323 pruva-verify CVE-2026-66902 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00323&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00323/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-66902 - Source: https://github.com/advisories/GHSA-vjmh-7f4c-cx88 ## Artifacts - bundle/repro/rca_report.md (analysis, 9017 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 13992 bytes) - bundle/logs/attempt_fixed_1.log (log, 212 bytes) - bundle/logs/attempt_fixed_2.log (log, 212 bytes) - bundle/logs/attempt_fixed_allow1.log (log, 206 bytes) - bundle/logs/attempt_vuln_1.log (log, 339 bytes) - bundle/logs/attempt_vuln_2.log (log, 339 bytes) - bundle/logs/reproduction_steps.log (log, 17833 bytes) - bundle/repro/adc/fixed_1.json (other, 786 bytes) - bundle/repro/adc/fixed_2.json (other, 786 bytes) - bundle/repro/adc/fixed_allow1.json (other, 796 bytes) - bundle/repro/adc/vuln_1.json (other, 784 bytes) - bundle/repro/adc/vuln_2.json (other, 784 bytes) - bundle/repro/markers/fixed_allow1.marker (other, 66 bytes) - bundle/repro/markers/vuln_1.marker (other, 60 bytes) - bundle/repro/markers/vuln_2.marker (other, 60 bytes) - bundle/repro/observations/fixed_1.json (other, 264 bytes) - bundle/repro/observations/fixed_2.json (other, 264 bytes) - bundle/repro/observations/fixed_allow1.json (other, 283 bytes) - bundle/repro/observations/vuln_1.json (other, 259 bytes) - bundle/repro/observations/vuln_2.json (other, 259 bytes) - bundle/repro/runtime_manifest.json (other, 1689 bytes) - bundle/repro/trigger.pl (other, 1082 bytes) - bundle/repro/validation_verdict.json (other, 934 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00323 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00323/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00323 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev