{"repro_id":"REPRO-2026-00323","version":6,"title":"Google::Auth for Perl command injection: external_account credentials JSON executable run via ungated system() → RCE","repro_type":"security","status":"published","severity":"critical","description":"Google::Auth versions before 0.06 for Perl run a command named in an external_account credentials JSON via an ungated system call — CWE-78 OS command injection leading to remote code execution.","root_cause":"# CVE-2026-66902 — Root Cause Analysis\n\n## Summary\n\nGoogle::Auth for Perl (CPAN distribution `Google-Auth`, maintained at\n`GoogleCloudPlatform/google-auth-library-perl`) executes a command taken verbatim\nfrom an external_account credentials JSON file through a single-argument\n`system($command)` call. In versions before 0.06 there is no opt-in gate: any\napplication that builds Application Default Credentials from a configuration it\ndoes not fully control runs the embedded shell command (with full `/bin/sh -c`\ninterpretation and attacker-chosen environment variables) with the privileges of\nthe application process. This is CWE-78 OS command injection leading to arbitrary\nOS command execution.\n\n## Impact\n\n- **Package/component:** CPAN `Google-Auth` (`Google::Auth`),\n  `lib/Google/Auth/ExternalAccountCredentials/Pluggable.pm`\n- **Affected versions:** < 0.06 (verified at commit\n  `913fb1780202c1ee9dd640c28c01549903f8e23a` = fix commit parent; packaged as 0.05)\n- **Risk level:** Critical. Any service/workload that consumes a credentials JSON\n  from an untrusted or partially trusted source (mounted config, user-supplied\n  file, CI artifact) executes attacker-chosen shell commands as the application\n  user.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** arbitrary OS command execution (RCE) in the\n  application process.\n- **Reproduced impact from this run:** arbitrary shell command execution in the\n  Perl application process. The embedded command used shell output redirection to\n  write a unique attacker-chosen marker file, and consumed attacker-controlled\n  environment variables copied from the same JSON (`environment_variables` map) —\n  proving both full shell interpretation and environment injection.\n- **Parity:** `full`.\n- **Not demonstrated:** nothing material; the claimed impact (code/command\n  execution) was reproduced directly, twice, against the real library entrypoint.\n\n## Root Cause\n\n`Google::Auth::ExternalAccountCredentials::Pluggable::retrieve_subject_token()`\n(in versions < 0.06) does the following with zero validation and no opt-in:\n\n1. Copies every entry of `credential_source.executable.environment_variables`\n   from the credentials JSON into `%ENV`.\n2. Reads `credential_source.executable.command` and runs\n   `capture { system($command) }`. Because `system()` receives a single string,\n   Perl invokes `/bin/sh -c`, giving the attacker pipes, redirection, command\n   substitution, and all shell metacharacters.\n\nDispatch reaches this subclass automatically: `Google::Auth->default()` →\n`Google::Auth::DefaultCredentials->from_env()` reads the JSON named by\n`GOOGLE_APPLICATION_CREDENTIALS` → `make_creds()` in\n`ExternalAccountCredentials.pm` selects the `Pluggable` subclass whenever\n`credential_source.executable` exists → construction succeeds with only\n`audience`, `subject_token_type`, `token_url`, and `credential_source` → the\nfirst `fetch_access_token()` (which every consuming application performs to use\nthe credential) calls `retrieve_subject_token()` and executes the command before\nany network access.\n\nFix commit `c95c77e70bec94f17e239d88050f843ea1cade95` (released as 0.06) adds an\nopt-in gate at the top of `retrieve_subject_token()` that throws unless\n`GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1`, plus a bounded execution timeout,\nschema validation of the command output, and URL domain validation in the base\nclass. Version 0.10 additionally parses with `Text::ParseWords` and uses indirect\n`system` exec.\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh` (self-contained; re-runnable).\n2. The script:\n   - Clones `GoogleCloudPlatform/google-auth-library-perl` (into the prepared\n     project cache when available) and resolves the vulnerable checkout as\n     `c95c77e70bec94f17e239d88050f843ea1cade95^` (= `913fb17`) and the fixed\n     checkout as the fix commit itself; verifies the gate string is absent in the\n     vulnerable tree and present in the fixed tree.\n   - Installs the pure-Perl runtime dependencies (Moo, Capture::Tiny,\n     LWP::UserAgent, Log::Any, Throwable, URI) via apt when permitted, otherwise\n     into a bundle-local `INSTALL_BASE` with `cpan`, then builds the real module\n     including its XS component (`perl Makefile.PL && make`) for both versions.\n   - Generates an attacker-controlled `external_account` credentials JSON whose\n     `credential_source.executable.command` writes a unique marker file using\n     shell redirection and attacker-injected environment variables, and points\n     `token_url` at a closed localhost port so the STS exchange fails fast after\n     the command has already executed.\n   - Invokes the real ADC flow as a CLI command:\n     `GOOGLE_APPLICATION_CREDENTIALS=<json> perl -I... trigger.pl`, where\n     `trigger.pl` calls `Google::Auth->default()` and then\n     `fetch_access_token()` — exactly the sequence a real application performs.\n   - Runs the matrix: 2 vulnerable attempts, 2 fixed attempts (default gated),\n     and 1 fixed attempt with the opt-in gate set (positive control).\n3. Expected evidence: both vulnerable attempts create their unique marker files;\n   both fixed attempts throw\n   `Pluggable credentials are not enabled. Set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1`\n   and create no marker; the opt-in control creates its marker again.\n\n## Evidence\n\n- Main log: `bundle/logs/reproduction_steps.log`\n- Per-attempt process logs: `bundle/logs/attempt_vuln_1.log`,\n  `bundle/logs/attempt_vuln_2.log`, `bundle/logs/attempt_fixed_1.log`,\n  `bundle/logs/attempt_fixed_2.log`, `bundle/logs/attempt_fixed_allow1.log`\n- Marker files (written by the injected shell command through `/bin/sh -c`):\n  `bundle/repro/markers/vuln_1.marker`,\n  `bundle/repro/markers/vuln_2.marker`,\n  `bundle/repro/markers/fixed_allow1.marker`\n- Per-attempt observation JSONs: `bundle/repro/observations/*.json`\n- Attacker configs used: `bundle/repro/adc/*.json`\n- Runtime manifest: `bundle/repro/runtime_manifest.json`\n\nKey excerpts (identical across two consecutive runs):\n\n```\n[run] vuln attempt 1 ... \nRESULT: credentials class = Google::Auth::ExternalAccountCredentials::Pluggable\nRESULT: fetch_access_token error: Token exchange failed with status 500: Can't connect to 127.0.0.1:9\n[run] vuln attempt 1: MARKER CREATED -> pwned-via-CVE-2026-66902 vuln attempt 1 shell+env injection\n[matrix] vulnerable attempts with marker: 2/2\n\nRESULT: fetch_access_token error: Pluggable credentials are not enabled. Set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1 to enable.\n[matrix] fixed attempts blocked (no marker): 2/2\n\n[run] fixed attempt allow1: MARKER CREATED -> pwned-via-CVE-2026-66902 fixed attempt allow1 shell+env injection\n[matrix] fixed+opt-in attempts with marker: 1/1\n=== RESULT: CONFIRMED - command injection via Pluggable external_account credentials ===\n```\n\nEnvironment: Ubuntu 24.04, perl 5.38.2 (x86_64-linux-gnu-thread-multi),\nOpenSSL 3.0.13 headers for the XS build, pure-Perl deps installed into\n`bundle/repro/deps` (cpan `INSTALL_BASE`). No sanitizers, no mocks, no network\ninteraction with Google endpoints (token_url is `http://127.0.0.1:9/v1/token`,\nwhich fails *after* the injected command has executed).\n\n## Recommendations / Next Steps\n\n- Upgrade to Google-Auth 0.06 or later; 0.06 throws unless\n  `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1` is set, and 0.10 additionally\n  shell-parses the command with `Text::ParseWords` and uses indirect (list-form)\n  `system` exec, removing `/bin/sh -c` interpretation.\n- Treat every credentials JSON reachable by the ADC flow\n  (`GOOGLE_APPLICATION_CREDENTIALS`, well-known paths) as executable code: restrict\n  write access, prefer trusted provisioning, and avoid setting the opt-in gate.\n- Regression testing: the upstream fix commit already adds tests\n  (`t/16-pluggable-credentials.t`); downstream should additionally test that a\n  `credential_source.executable` config without the gate never spawns a process.\n\n## Additional Notes\n\n- **Idempotency:** the script was executed twice consecutively (plus two more\n  times after adding unique per-attempt markers/observations) — every run\n  produced the full matrix result (2/2 vulnerable markers, 2/2 fixed blocked,\n  1/1 opt-in control) and exit code 0.\n- The command executes *before* the STS token exchange; the proof intentionally\n  uses a closed-loopback `token_url` so no external network call is needed and\n  the marker is created regardless of the later (expected) STS failure.\n- Both the claimed entrypoint variants are supported: the primary proof uses\n  `Google::Auth->default()` (requires the XS build, which the script performs);\n  if the XS toolchain were unavailable the script falls back to\n  `Google::Auth::DefaultCredentials->from_env()`, which is the same dispatch\n  path named in the claim.\n- Edge case: the vulnerable code only executes the command when a token is\n  fetched, not at config parse time — matching real application behavior, since\n  any consumer of the credential calls `fetch_access_token()` to use it.\n","cve_id":"CVE-2026-66902","cwe_id":"CWE-78","source_url":"https://github.com/advisories/GHSA-vjmh-7f4c-cx88","reproduced_at":"2026-08-23T15:38:13.515613+00:00","duration_secs":1012.0,"tool_calls":169,"handoffs":2,"total_cost_usd":3.655322,"agent_costs":{"claim_matcher":0.014882,"judge":0.783379,"learning_policy":0.014355,"repro":1.553529,"support":0.225049,"vuln_variant":1.064128},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.014882},"judge":{"gpt-5.6-sol":0.783379},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.014355},"repro":{"accounts/fireworks/models/kimi-k3":1.553529},"support":{"accounts/fireworks/models/kimi-k3":0.225049},"vuln_variant":{"accounts/fireworks/models/kimi-k3":1.064128}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-23T15:38:14.250181+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":9017,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":13992,"category":"reproduction_script"},{"path":"bundle/logs/attempt_fixed_1.log","filename":"attempt_fixed_1.log","size":212,"category":"log"},{"path":"bundle/logs/attempt_fixed_2.log","filename":"attempt_fixed_2.log","size":212,"category":"log"},{"path":"bundle/logs/attempt_fixed_allow1.log","filename":"attempt_fixed_allow1.log","size":206,"category":"log"},{"path":"bundle/logs/attempt_vuln_1.log","filename":"attempt_vuln_1.log","size":339,"category":"log"},{"path":"bundle/logs/attempt_vuln_2.log","filename":"attempt_vuln_2.log","size":339,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":17833,"category":"log"},{"path":"bundle/repro/adc/fixed_1.json","filename":"fixed_1.json","size":786,"category":"other"},{"path":"bundle/repro/adc/fixed_2.json","filename":"fixed_2.json","size":786,"category":"other"},{"path":"bundle/repro/adc/fixed_allow1.json","filename":"fixed_allow1.json","size":796,"category":"other"},{"path":"bundle/repro/adc/vuln_1.json","filename":"vuln_1.json","size":784,"category":"other"},{"path":"bundle/repro/adc/vuln_2.json","filename":"vuln_2.json","size":784,"category":"other"},{"path":"bundle/repro/markers/fixed_allow1.marker","filename":"fixed_allow1.marker","size":66,"category":"other"},{"path":"bundle/repro/markers/vuln_1.marker","filename":"vuln_1.marker","size":60,"category":"other"},{"path":"bundle/repro/markers/vuln_2.marker","filename":"vuln_2.marker","size":60,"category":"other"},{"path":"bundle/repro/observations/fixed_1.json","filename":"fixed_1.json","size":264,"category":"other"},{"path":"bundle/repro/observations/fixed_2.json","filename":"fixed_2.json","size":264,"category":"other"},{"path":"bundle/repro/observations/fixed_allow1.json","filename":"fixed_allow1.json","size":283,"category":"other"},{"path":"bundle/repro/observations/vuln_1.json","filename":"vuln_1.json","size":259,"category":"other"},{"path":"bundle/repro/observations/vuln_2.json","filename":"vuln_2.json","size":259,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1689,"category":"other"},{"path":"bundle/repro/trigger.pl","filename":"trigger.pl","size":1082,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":934,"category":"other"}]}