# Patch Analysis — CVE-2026-55175 (Spinnaker Rosco Kustomize unsafe YAML tag RCE)

## Fix Commit

`f5cec213f8cf207843ed5a6929395960a1ca094f` — "fix(kustomize): Add tests & fixes for kustomize behaviors (#7714)"

## What the Fix Changes

**File:** `rosco/rosco-manifests/src/main/java/com/netflix/spinnaker/rosco/manifests/kustomize/KustomizationFileReader.java`
**Method:** `convert(Artifact artifact)`

Before the fix, untrusted `kustomization.yaml` content fetched from Clouddriver was parsed with
SnakeYAML's general `Constructor(Kustomization.class)`, which honors arbitrary YAML tags and
instantiates the referenced Java objects during deserialization:

```java
Representer representer = new Representer();
representer.getPropertyUtils().setSkipMissingProperties(true);
return new Yaml(new Constructor(Kustomization.class), representer).load(downloadFile(artifact));
```

After the fix, parsing is split into two safe stages:

```java
private static final ObjectMapper objectMapper =
    new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
...
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
Map<String, Object> rawMap = yaml.load(downloadFile(artifact));
return objectMapper.convertValue(rawMap, Kustomization.class);
```

1. `SafeConstructor` only constructs standard YAML types (`Map`, `List`, `String`, numbers,
   booleans, `null`). It rejects arbitrary `!!<fqcn>` tags with a `ConstructorException`, blocking
   CVE-2022-1471 / CWE-502 style arbitrary object instantiation.
2. The resulting safe raw `Map` is then mapped to the `Kustomization` POJO via Jackson
   `ObjectMapper.convertValue()`. The `ObjectMapper` is a plain instance with default typing
   **disabled** and `FAIL_ON_UNKNOWN_PROPERTIES=false`, so no polymorphic deserialization is
   possible from a `Map` source. The `Kustomization` mapping class and its field types
   (`ConfigMapGenerator`, `Patch`, `PatchesJson6902`, `Target`) carry no `@JsonTypeInfo` /
   `@JsonSubTypes` annotations, so `convertValue` performs only field-by-field POJO mapping.

The fix also adds a regression test suite
(`KustomizeSafeConstructorTest.java`, 243 lines) covering `ScriptEngineManager`, `URL`,
map-field, and root-tag-override vectors.

## What Assumptions the Fix Makes

1. **Single sink assumption.** The fix assumes that all attacker-controlled `kustomization.yaml`
   parsing flows through `KustomizationFileReader.convert()`. This is correct: every code path
   that fetches and parses a kustomization file calls
   `kustomizationFileReader.getKustomization(...)` → `convert(...)`.
2. **SafeConstructor is sufficient.** The fix assumes `SafeConstructor` blocks all arbitrary
   object construction. This holds for SnakeYAML `!!` global tags and `!<fqcn>` verbatim tags.
3. **Jackson convertValue is non-polymorphic.** The fix assumes `convertValue(Map, Kustomization)`
   cannot trigger arbitrary type instantiation. This holds because the target type graph has no
   polymorphic type info and default typing is off.

## What Code Paths / Inputs the Fix Covers

- `POST /api/v2/manifest/bake/KUSTOMIZE` (parent entry point) — covered.
- `POST /api/v2/manifest/bake/KUSTOMIZE4` (sibling entry point) — covered. `V2BakeryController`
  dispatches both `KUSTOMIZE` and `KUSTOMIZE4` to `KustomizeBakeManifestService` (its `handles()`
  set is `{KUSTOMIZE, KUSTOMIZE4}`), and both flow through `KustomizeTemplateUtils.buildBakeRecipe`
  → `getFilesFromArtifact` → `KustomizationFileReader.getKustomization` → `convert()`.
- Recursive nested kustomization references: `getFilesFromArtifact` recursively fetches and parses
  kustomization files referenced via the `resources` field. Each nested file is parsed through the
  same `getKustomization` → `convert()` path, so the fix covers the full dependency tree.

## What the Fix Does NOT Cover / Gaps

- **`git/repo` artifact bake path.** `KustomizeTemplateUtils.buildBakeRecipeFromGitRepo` downloads
  a tarball and runs the `kustomize` binary directly; it does **not** call
  `kustomizationFileReader.getKustomization()` at all, so no Java YAML deserialization occurs on
  that path. This is not a gap for the YAML-tag RCE class (no SnakeYAML sink is reached), but it
  means the kustomization file is only ever validated by the external `kustomize` binary, not by
  Rosco Java code.
- **Other bake services.** `CloudFoundryBakeManifestService` uses `new Yaml()` (SnakeYAML default
  `SafeConstructor`) and loads into a `Map`, which is safe by default. No `Constructor(<class>)`
  usage exists elsewhere in `rosco-manifests` (verified by source search). The fix correctly
  scoped the change to the single unsafe sink.

## Threat Model / Security Policy

No `SECURITY.md` or explicit threat-model document was found in the Spinnaker repository. The CVE
itself establishes that unsafe YAML tag processing reaching RCE on Rosco pods is an in-scope
security vulnerability, so the Kustomize bake entry points are within the protected trust
boundary (authenticated API caller → Rosco pod).

## Behavior Before vs After the Fix

| Aspect | Vulnerable (`d7d131a1b1`) | Fixed (`f5cec213f8`) |
|---|---|---|
| `kustomization.yaml` with `!!javax.script.ScriptEngineManager` tag | Object instantiated → RCE | `ConstructorException` / `IllegalArgumentException`, request rejected (HTTP 400) |
| KUSTOMIZE4 endpoint with same payload | Object instantiated → RCE (confirmed by this variant run) | Rejected, no payload execution (HTTP 400) |
| Legitimate kustomization.yaml | Parsed into `Kustomization` POJO | Parsed into safe `Map` then mapped to `Kustomization` POJO (behavior preserved) |

## Completeness Assessment

The fix is **complete** for the unsafe-YAML-tag RCE class. It addresses the root cause at the
single shared sink (`KustomizationFileReader.convert`), so all entry points that reach that sink
— including the sibling `KUSTOMIZE4` endpoint and recursive nested kustomization references — are
covered. No bypass was found. The fix is minimal, correct, and regression-tested.
