# Variant RCA Report — CVE-2026-55175 (KUSTOMIZE4 alternate bake endpoint)

## Summary

CVE-2026-55175 is an unsafe YAML tag processing RCE in Spinnaker Rosco's Kustomize bake path.
The original reproduction proved RCE via `POST /api/v2/manifest/bake/KUSTOMIZE`. This variant
analysis evaluated the sibling entry point `POST /api/v2/manifest/bake/KUSTOMIZE4`, which is
dispatched by the same `V2BakeryController` to `KustomizeBakeManifestService` (its `handles()` set
accepts both `KUSTOMIZE` and `KUSTOMIZE4`) and reaches the **same** sink
`KustomizationFileReader.convert()`. The KUSTOMIZE4 variant was confirmed as a real alternate
trigger of the same root cause on the vulnerable build (`payloadExecuted=true`), and was confirmed
**blocked** by the existing `SafeConstructor` fix on the fixed build (`payloadExecuted=false`,
HTTP 400). **No bypass was found.** The fix addresses the root cause at the shared sink, so it
covers both the `KUSTOMIZE` and `KUSTOMIZE4` dispatch paths.

## Fix Coverage / Assumptions

- **Invariant the fix relies on:** all attacker-controlled `kustomization.yaml` parsing flows
  through a single sink, `KustomizationFileReader.convert()`, which the fix rewrites to use
  `SafeConstructor` + Jackson `ObjectMapper.convertValue()` instead of
  `Constructor(Kustomization.class)`.
- **Code paths explicitly covered:** `POST /api/v2/manifest/bake/KUSTOMIZE`,
  `POST /api/v2/manifest/bake/KUSTOMIZE4`, and the recursive nested-kustomization path in
  `KustomizeTemplateUtils.getFilesFromArtifact` (each nested file is parsed via
  `getKustomization` → `convert()`).
- **What the fix does NOT cover:** the `git/repo` artifact bake path
  (`KustomizeTemplateUtils.buildBakeRecipeFromGitRepo`) does not invoke
  `kustomizationFileReader.getKustomization()` at all — it downloads a tarball and runs the
  `kustomize` binary. There is no Java SnakeYAML sink on that path, so it is not a gap for the
  YAML-tag RCE class. No other `Constructor(<class>)` usage exists in `rosco-manifests`
  (`CloudFoundryBakeManifestService` uses `new Yaml()`, i.e. the default `SafeConstructor`).

## Variant / Alternate Trigger

**Entry point:** `POST /api/v2/manifest/bake/KUSTOMIZE4` with request body
`{"templateRenderer":"KUSTOMIZE4", "inputArtifact":{"type":"github/file", ...}}`.

**Code path:**
1. `V2BakeryController.doBake("KUSTOMIZE4", request)` — `rosco-web/.../V2BakeryController.java`
2. `KustomizeBakeManifestService.handles("KUSTOMIZE4")` → true (supportedTemplates =
   `{KUSTOMIZE, KUSTOMIZE4}`)
3. `KustomizeBakeManifestService.bake(...)` → `KustomizeTemplateUtils.buildBakeRecipe(...)`
4. `oldBuildBakeRecipe` (artifact type `github/file`, not `git/repo`) → `getArtifacts` →
   `getFilesFromArtifact`
5. `KustomizationFileReader.getKustomization(...)` → `convert(artifact)` — **shared sink**
6. Vulnerable: `new Yaml(new Constructor(Kustomization.class), representer).load(...)` →
   `!!javax.script.ScriptEngineManager` tag honored → `ScriptEngineFactory` loaded from attacker
   URLClassLoader → constructor runs → RCE.
   Fixed: `new Yaml(new SafeConstructor(...)).load(...)` rejects the tag → `convertValue` never
   sees it → HTTP 400.

This is materially different from the parent only in the **entry point** (different `{type}` path
variable and `templateRenderer` value); it reaches the identical sink and crosses the same trust
boundary (authenticated API caller → Rosco pod). Because the fix is applied at the shared sink, it
is not a bypass.

### Other candidates considered and ruled out

| Candidate | Reaches SnakeYAML sink? | Same root cause? | Verdict |
|---|---|---|---|
| KUSTOMIZE4 endpoint | Yes, same `convert()` | Yes | Alternate trigger; covered by fix (tested). |
| Recursive nested kustomization refs (`resources` → subfolder) | Yes, same `convert()` per nested file | Yes | Alternate data path; covered by fix (same sink). |
| `git/repo` artifact bake | No (no `getKustomization` call) | No | Out of scope for YAML-tag RCE class. |
| `CloudFoundryBakeManifestService` `new Yaml().load()` | Uses default `SafeConstructor` | No | Safe by default; not the same root cause. |
| Jackson `convertValue` polymorphic instantiation | `ObjectMapper` has default typing off; `Kustomization` has no `@JsonTypeInfo` | No | Not exploitable. |

## Impact

- **Package/component affected:** Spinnaker Rosco — `io.spinnaker.rosco:rosco-manifests` /
  `rosco-web`, Kustomize bake flow.
- **Affected versions (as tested):** vulnerable `d7d131a1b1fcb831256034f1e8d023f6e9dc4fc3`
  (parent of fix); fixed `f5cec213f8cf207843ed5a6929395960a1ca094f`. Advisory-listed affected
  ranges: prior to 2026.1.1 / 2026.0.3 / 2025.4.4 / 2025.3.4.
- **Risk level:** High — authenticated actor able to perform Kustomize/KUSTOMIZE4 bakes can
  execute arbitrary code inside Rosco pods on vulnerable builds.

## Impact Parity

- **Disclosed/claimed maximum impact:** RCE on Rosco pods during Kustomize bake operations.
- **Reproduced impact from this variant run:** On the **vulnerable** build, the KUSTOMIZE4
  endpoint instantiated the attacker `ScriptEngineFactory` from a URLClassLoader and its
  constructor executed (marker file written), proving RCE via the alternate entry point
  (`payloadExecuted=true`, HTTP 500 `ClassCastException` after the payload ran). On the **fixed**
  build the same payload was rejected without execution (`payloadExecuted=false`, HTTP 400).
- **Parity:** `full` for the alternate-trigger demonstration on the vulnerable version; `none` for
  bypass (the fixed version blocks it).
- **Not demonstrated:** A bypass of the fix — the fix covers the KUSTOMIZE4 path.

## Root Cause

The underlying bug is that untrusted `kustomization.yaml` was parsed with SnakeYAML's general
`Constructor(Kustomization.class)`, which honors arbitrary YAML tags and instantiates the
referenced Java objects. `KUSTOMIZE` and `KUSTOMIZE4` are two HTTP dispatch labels for the same
`KustomizeBakeManifestService`; both converge on `KustomizationFileReader.convert()`. The fix
(commit `f5cec213f8cf207843ed5a6929395960a1ca094f`) replaces the unsafe `Constructor` with
`SafeConstructor` and maps the safe `Map` to the POJO via Jackson `convertValue`. Because the fix
is at the shared sink, every entry point that reaches `convert()` — including `KUSTOMIZE4` — is
protected.

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh`
2. **What the script does:**
   - Reuses the prepared Spinnaker repository from the project cache.
   - Resolves the vulnerable parent commit `d7d131a1b1fcb831256034f1e8d023f6e9dc4fc3` and the
     fixed commit `f5cec213f8cf207843ed5a6929395960a1ca094f`.
   - Writes a Spring Boot integration test
     `CVE202655175VariantKustomize4Test.java` that starts a real Rosco HTTP server, stands up a
     WireMock Clouddriver peer serving a malicious `kustomization.yaml` with a
     `!!javax.script.ScriptEngineManager` tag, builds a payload JAR whose `ScriptEngineFactory`
     constructor writes a marker file, and sends a real HTTP POST to
     `/api/v2/manifest/bake/KUSTOMIZE4` with `templateRenderer=KUSTOMIZE4`.
   - Runs the test on the **vulnerable** commit (expects `payloadExecuted=true`), then on the
     **fixed** commit (expects `payloadExecuted=false`, HTTP 400).
   - Restores the repository to its original HEAD and removes the temporary test file.
   - Exit 0 = bypass (variant reproduces on fixed); Exit 1 = no bypass.
3. **Expected evidence:**
   - `bundle/vuln_variant/vulnerable_attempt_1.evidence.txt`:
     `payloadExecuted=true`, `clouddriverReached=true`, HTTP 500 `ClassCastException`.
   - `bundle/vuln_variant/fixed_attempt_1.evidence.txt`:
     `payloadExecuted=false`, `clouddriverReached=true`, HTTP 400.
   - `bundle/vuln_variant/validation_verdict.json`: `is_bypass=false`,
     `reproduced_on_vulnerable=true`, `reproduced_on_fixed=false`.

## Evidence

- **Logs:** `bundle/logs/vuln_variant/reproduction_steps.log`,
  `bundle/logs/vuln_variant/vulnerable_attempt_1.log`,
  `bundle/logs/vuln_variant/fixed_attempt_1.log`.
- **Evidence files:** `bundle/vuln_variant/vulnerable_attempt_1.evidence.txt`,
  `bundle/vuln_variant/fixed_attempt_1.evidence.txt` (and `.payload` marker).
- **Key excerpts:**

  Vulnerable (KUSTOMIZE4):
  ```
  mode=vulnerable
  renderer=KUSTOMIZE4
  endpoint=/api/v2/manifest/bake/KUSTOMIZE4
  status=500
  clouddriverReached=true
  payloadExecuted=true
  body=...java.lang.ClassCastException: class javax.script.ScriptEngineManager cannot be cast to class java.lang.String...
  ```

  Fixed (KUSTOMIZE4):
  ```
  mode=fixed
  renderer=KUSTOMIZE4
  endpoint=/api/v2/manifest/bake/KUSTOMIZE4
  status=400
  clouddriverReached=true
  payloadExecuted=false
  body=...Unable to find any kustomization file for root...
  ```

- **Environment:** OpenJDK 17.0.19, Gradle 7.6.1 (`--no-daemon --max-workers=2 --rerun-tasks`),
  Spring Boot test RANDOM_PORT, WireMock Clouddriver peer.
- **Tested source identity:** `bundle/vuln_variant/source_identity.json` (fixed commit
  `f5cec213f8cf207843ed5a6929395960a1ca094f`, vulnerable `d7d131a1b1fcb831256034f1e8d023f6e9dc4fc3`).

## Recommendations / Next Steps

- **No code change required.** The existing `SafeConstructor` fix already covers the KUSTOMIZE4
  dispatch path because the fix is at the shared `KustomizationFileReader.convert()` sink.
- **Defense in depth (optional):** Add an explicit regression test asserting that the
  `KUSTOMIZE4` endpoint rejects a `!!`-tagged `kustomization.yaml` (the existing
  `KustomizeSafeConstructorTest` covers `convert()` directly but not the KUSTOMIZE4 HTTP
  dispatch). This variant test (`CVE202655175VariantKustomize4Test`) can serve as that regression.
- **Ensure future bake renderers that parse untrusted YAML reuse the SafeConstructor pattern**
  rather than `Constructor(<class>)`, and avoid introducing `@JsonTypeInfo`/default typing on
  `ObjectMapper` instances used to map untrusted maps.

## Additional Notes

- **Idempotency:** The script was run twice; both runs produced identical evidence
  (vulnerable `payloadExecuted=true`, fixed `payloadExecuted=false`, exit 1) and restored the
  repository to its original HEAD (`f5cec213f8cf207843ed5a6929395960a1ca094f`) with the temporary
  test file removed.
- **Repo state:** No checkout state was left changed; the shared project-cache repo is back on the
  fixed commit, matching the state used by the repro stage.
- **Trust boundary:** The KUSTOMIZE4 endpoint is an authenticated HTTP API on the Rosco pod; an
  authorized caller supplying a `github/file` input artifact crosses the network trust boundary
  into the Rosco JVM. No `SECURITY.md` excluding this attack class was found in the repository.
- **Limitation:** Only the KUSTOMIZE4 alternate entry point was exercised at runtime. The
  recursive-nested-kustomization and `CloudFoundryBakeManifestService` candidates were ruled out
  by source inspection (same shared sink / safe-by-default) as documented above; additional
  runtime attempts would re-exercise the identical `convert()` sink and not change the verdict.
