fix(kubernetes): prevent kubectl flag injection in pod exec #6

Merged
stack72 merged 3 commits from fix/kubernetes-exec-flag-injection into main 2026-05-27 21:31:00 +00:00
Owner

Summary

Security fix for kubectl flag injection vulnerability in the @swamp/kubernetes pod exec method, plus a CI fix for the Write tool blocking in review agents.

Flag injection in pod exec (kubernetes/extensions/models/pod.ts)

The exec method passes args.podName directly into the kubectl args array at the second position (immediately after "exec"). Because Deno.Command passes each element verbatim to the OS, a pod name starting with - is interpreted by kubectl as a flag rather than a positional argument.

Example exploit: A pod name like --as=system:masters would cause kubectl to impersonate a cluster-admin identity:

kubectl exec --as=system:masters -n default -- cat /etc/passwd

The -- separator at the end of the args only separates kubectl options from the exec command — it does not protect the positional pod name argument that comes before it.

Fix: Added a Kubernetes DNS label validation guard (/^[a-z0-9][a-z0-9\-.]*$/) at the top of the exec method. This rejects any pod name that doesn't conform to Kubernetes naming rules, which inherently blocks flag-like values starting with -. This is the correct approach because:

  • Kubernetes pod names are always valid DNS labels (lowercase alphanumeric, -, .)
  • No valid pod name can ever start with -
  • Validating at the model layer catches the issue before any subprocess is spawned

CI review agent Write tool blocking (.forgejo/workflows/ci.yml)

The --allowedTools list for the three CI review jobs (claude-review, claude-adversarial-review, claude-ci-security-review) did not include Write. The review prompts instruct Claude to use tee to write output, but the model sometimes chooses the Write tool instead. Since Write wasn't in the allowed list, it triggered an approval prompt — which blocks indefinitely in non-interactive CI mode, preventing the review from completing.

Fix: Added Write to --allowedTools for all three review jobs.

Version bump

All 15 model files and manifest.yaml bumped from 2026.05.27.12026.05.27.2 with upgrade entries added to each model.

Changed files

  • kubernetes/extensions/models/pod.ts — security fix + version bump + upgrade entry
  • kubernetes/extensions/models/{configmap,deployment,event,hpa,ingress,job,namespace,netpol,node,pod_summary,pvc,rbac,secret,service}.ts — version bump + upgrade entry
  • kubernetes/manifest.yaml — version bump
  • .forgejo/workflows/ci.yml — add Write to allowedTools

Test plan

  • deno check extensions/models/*.ts — all 15 models pass
  • deno lint extensions/models/ — clean
  • deno fmt --check extensions/models/ — clean
  • Verify exec method rejects pod names starting with - at runtime
  • Verify CI review agents can complete without Write tool approval prompts

Issue originally identified by the CI adversarial code review agent.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

## Summary Security fix for kubectl flag injection vulnerability in the `@swamp/kubernetes` pod exec method, plus a CI fix for the Write tool blocking in review agents. ### Flag injection in pod exec (`kubernetes/extensions/models/pod.ts`) The `exec` method passes `args.podName` directly into the kubectl args array at the second position (immediately after `"exec"`). Because `Deno.Command` passes each element verbatim to the OS, a pod name starting with `-` is interpreted by kubectl as a flag rather than a positional argument. **Example exploit:** A pod name like `--as=system:masters` would cause kubectl to impersonate a cluster-admin identity: ``` kubectl exec --as=system:masters -n default -- cat /etc/passwd ``` The `--` separator at the end of the args only separates kubectl options from the exec command — it does not protect the positional pod name argument that comes before it. **Fix:** Added a Kubernetes DNS label validation guard (`/^[a-z0-9][a-z0-9\-.]*$/`) at the top of the exec method. This rejects any pod name that doesn't conform to Kubernetes naming rules, which inherently blocks flag-like values starting with `-`. This is the correct approach because: - Kubernetes pod names are always valid DNS labels (lowercase alphanumeric, `-`, `.`) - No valid pod name can ever start with `-` - Validating at the model layer catches the issue before any subprocess is spawned ### CI review agent Write tool blocking (`.forgejo/workflows/ci.yml`) The `--allowedTools` list for the three CI review jobs (claude-review, claude-adversarial-review, claude-ci-security-review) did not include `Write`. The review prompts instruct Claude to use `tee` to write output, but the model sometimes chooses the `Write` tool instead. Since `Write` wasn't in the allowed list, it triggered an approval prompt — which blocks indefinitely in non-interactive CI mode, preventing the review from completing. **Fix:** Added `Write` to `--allowedTools` for all three review jobs. ### Version bump All 15 model files and `manifest.yaml` bumped from `2026.05.27.1` → `2026.05.27.2` with upgrade entries added to each model. ## Changed files - `kubernetes/extensions/models/pod.ts` — security fix + version bump + upgrade entry - `kubernetes/extensions/models/{configmap,deployment,event,hpa,ingress,job,namespace,netpol,node,pod_summary,pvc,rbac,secret,service}.ts` — version bump + upgrade entry - `kubernetes/manifest.yaml` — version bump - `.forgejo/workflows/ci.yml` — add Write to allowedTools ## Test plan - [x] `deno check extensions/models/*.ts` — all 15 models pass - [x] `deno lint extensions/models/` — clean - [x] `deno fmt --check extensions/models/` — clean - [ ] Verify exec method rejects pod names starting with `-` at runtime - [ ] Verify CI review agents can complete without Write tool approval prompts --- Issue originally identified by the CI adversarial code review agent. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix(kubernetes): prevent kubectl flag injection in pod exec method
Some checks failed
CI / kubernetes - lockfile up to date (pull_request) Successful in 30s
CI / workflows/s3-bootstrap - test (pull_request) Has been skipped
CI / cve/mini-shai-hulud - check (pull_request) Has been skipped
CI / cve/dirtyfrag - test (pull_request) Has been skipped
CI / CI Security Review (pull_request) Failing after 3m4s
CI / workflows/s3-bootstrap - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - fmt (pull_request) Has been skipped
CI / workflows/s3-bootstrap - check (pull_request) Has been skipped
CI / workflows/gcs-bootstrap - test (pull_request) Has been skipped
CI / Claude Code Review (pull_request) Successful in 5m5s
CI / Adversarial Code Review (pull_request) Successful in 4m41s
CI / workflows/gcs-bootstrap - fmt (pull_request) Has been skipped
CI / workflows/gcs-bootstrap - check (pull_request) Has been skipped
CI / workflows/s3-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / workflows/gcs-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / cve/dirtyfrag - lint (pull_request) Has been skipped
CI / Merge Gate (pull_request) Failing after 3s
CI / ssh - fmt (pull_request) Has been skipped
CI / workflows/gcs-bootstrap - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - check (pull_request) Has been skipped
CI / ssh - check (pull_request) Has been skipped
CI / ssh - test (pull_request) Has been skipped
CI / ssh - lint (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / workflows/s3-bootstrap - fmt (pull_request) Has been skipped
CI / ssh - lockfile up to date (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / kubernetes - check (pull_request) Successful in 40s
CI / kubernetes - lint (pull_request) Successful in 27s
CI / kubernetes - fmt (pull_request) Successful in 27s
5caedbe66e
Validate pod names against the Kubernetes DNS label pattern before passing
them to the kubectl args array, preventing flag injection via names starting
with '-' (e.g. --as=system:masters). Also add Write to CI review allowedTools
so the review agent can write output without blocking in non-interactive mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Critical / High

  1. Write tool included in LLM --allowedTools is overly broad.forgejo/workflows/ci.yml:730, 844, 935

    Vulnerability: All three Claude review jobs (claude-review, claude-adversarial-review, claude-ci-security-review) include Write in their --allowedTools list. The Write tool provides unrestricted file system write access within the runner workspace.

    Attack scenario: The LLM reads the content of every changed file during review. An attacker submits a PR containing a file with embedded prompt injection in its source code (e.g., comments, string literals, or docstrings designed to override the system prompt). If injection succeeds, the attacker-controlled LLM can use Write to:

    • Overwrite review-body.md with a fake "PASS — no issues found" verdict, causing the review gate to pass on malicious code.
    • Write to other workspace paths to influence subsequent steps.

    The Bash tool is correctly scoped to only tee and touch for review output — but Write bypasses that scoping entirely, since it can write to any file path.

    Fix: Remove Write from --allowedTools in all three Claude review steps. The agent only needs to produce review output, which is already covered by the scoped Bash commands (tee ${OUTPUT_DIR}/review-body.md:* and touch ${OUTPUT_DIR}/review-failed). The corrected tools list:

    --allowedTools "Read,Grep,Bash(git diff:*),Bash(git log:*),Bash(tee ${OUTPUT_DIR}/review-body.md:*),Bash(touch ${OUTPUT_DIR}/review-failed)"
    

Medium

  1. pull-requests: write permission may be unnecessary.forgejo/workflows/ci.yml:679, 793, 884

    The three Claude review jobs declare pull-requests: write at the job level. However, review comments are posted via direct Forgejo API calls using secrets.BOT_TOKEN, not via the GITHUB_TOKEN / runner token. If pull-requests: write is not consumed by any step using the runner token, it grants a broader permission scope than needed. Verify whether the Forgejo runner token actually requires this permission for the API calls; if not, remove it to follow least-privilege.

  2. Claude binary fetched via curl from remote URL.forgejo/workflows/ci.yml:714, 829, 920

    The Claude CLI is downloaded at runtime via curl from downloads.claude.ai. This is mitigated by SHA256 checksum verification (good), version pinning (good), and curl -f to fail on HTTP errors. This is comparable in security to SHA-pinned actions. However, vendoring the binary or using a first-party action would eliminate the runtime dependency on an external download server entirely. Noting as medium because the current checksumming is adequate but the pattern could be hardened further.

Low

No low-severity findings.

Security Positives

  • Safe trigger: pull_request (not pull_request_target) — PR code runs in the PR context, not the base repo context with secrets.
  • No direct interpolation of attacker-controlled event fields: PR title, body, and comment bodies are never spliced into prompts or run: blocks. The CHANGED_FILES variable contains only file paths from git diff.
  • Environment variable pattern for SHAs: BASE_SHA and HEAD_SHA are passed via env: and quoted in the shell, not interpolated directly — correct mitigation for expression injection.
  • Prompt hardening: All three prompt files (.forgejo/prompts/review.md, adversarial.md, ci-security.md) include security preambles instructing the model to treat PR content as untrusted data.
  • Tightly scoped Bash tool: The Bash tool is restricted to git diff, git log, tee, and touch — no curl, wget, or arbitrary command execution.
  • Actions from trusted publishers: actions/checkout@v6 and denoland/setup-deno@v2 are from trusted publishers with tag pins, acceptable per policy.
  • Secret scoping: ANTHROPIC_API_KEY and BOT_TOKEN are scoped to individual steps, not entire jobs.
  • Workflow-level contents: read: Sets a restrictive baseline; jobs that need write permissions override explicitly.
  • No auto-merge: The workflow cannot approve or merge PRs — it can only block via the merge gate.
  • Concurrency controls: Review jobs use cancel-in-progress: true to prevent stale reviews from posting.

Verdict

FAIL — The Write tool in the LLM --allowedTools list is overly broad and should be removed from all three Claude review jobs. This is straightforward to fix (delete Write, from the three --allowedTools strings). Once fixed, this workflow has solid security posture.

## CI Security Review ### Critical / High 1. **`Write` tool included in LLM `--allowedTools` is overly broad** — `.forgejo/workflows/ci.yml:730, 844, 935` **Vulnerability:** All three Claude review jobs (`claude-review`, `claude-adversarial-review`, `claude-ci-security-review`) include `Write` in their `--allowedTools` list. The `Write` tool provides unrestricted file system write access within the runner workspace. **Attack scenario:** The LLM reads the _content_ of every changed file during review. An attacker submits a PR containing a file with embedded prompt injection in its source code (e.g., comments, string literals, or docstrings designed to override the system prompt). If injection succeeds, the attacker-controlled LLM can use `Write` to: - Overwrite `review-body.md` with a fake "PASS — no issues found" verdict, causing the review gate to pass on malicious code. - Write to other workspace paths to influence subsequent steps. The `Bash` tool is correctly scoped to only `tee` and `touch` for review output — but `Write` bypasses that scoping entirely, since it can write to any file path. **Fix:** Remove `Write` from `--allowedTools` in all three Claude review steps. The agent only needs to produce review output, which is already covered by the scoped Bash commands (`tee ${OUTPUT_DIR}/review-body.md:*` and `touch ${OUTPUT_DIR}/review-failed`). The corrected tools list: ``` --allowedTools "Read,Grep,Bash(git diff:*),Bash(git log:*),Bash(tee ${OUTPUT_DIR}/review-body.md:*),Bash(touch ${OUTPUT_DIR}/review-failed)" ``` ### Medium 1. **`pull-requests: write` permission may be unnecessary** — `.forgejo/workflows/ci.yml:679, 793, 884` The three Claude review jobs declare `pull-requests: write` at the job level. However, review comments are posted via direct Forgejo API calls using `secrets.BOT_TOKEN`, not via the `GITHUB_TOKEN` / runner token. If `pull-requests: write` is not consumed by any step using the runner token, it grants a broader permission scope than needed. Verify whether the Forgejo runner token actually requires this permission for the API calls; if not, remove it to follow least-privilege. 2. **Claude binary fetched via `curl` from remote URL** — `.forgejo/workflows/ci.yml:714, 829, 920` The Claude CLI is downloaded at runtime via `curl` from `downloads.claude.ai`. This is mitigated by SHA256 checksum verification (good), version pinning (good), and `curl -f` to fail on HTTP errors. This is comparable in security to SHA-pinned actions. However, vendoring the binary or using a first-party action would eliminate the runtime dependency on an external download server entirely. Noting as medium because the current checksumming is adequate but the pattern could be hardened further. ### Low No low-severity findings. ### Security Positives - **Safe trigger**: `pull_request` (not `pull_request_target`) — PR code runs in the PR context, not the base repo context with secrets. - **No direct interpolation of attacker-controlled event fields**: PR title, body, and comment bodies are never spliced into prompts or `run:` blocks. The `CHANGED_FILES` variable contains only file paths from `git diff`. - **Environment variable pattern for SHAs**: `BASE_SHA` and `HEAD_SHA` are passed via `env:` and quoted in the shell, not interpolated directly — correct mitigation for expression injection. - **Prompt hardening**: All three prompt files (`.forgejo/prompts/review.md`, `adversarial.md`, `ci-security.md`) include security preambles instructing the model to treat PR content as untrusted data. - **Tightly scoped Bash tool**: The `Bash` tool is restricted to `git diff`, `git log`, `tee`, and `touch` — no `curl`, `wget`, or arbitrary command execution. - **Actions from trusted publishers**: `actions/checkout@v6` and `denoland/setup-deno@v2` are from trusted publishers with tag pins, acceptable per policy. - **Secret scoping**: `ANTHROPIC_API_KEY` and `BOT_TOKEN` are scoped to individual steps, not entire jobs. - **Workflow-level `contents: read`**: Sets a restrictive baseline; jobs that need write permissions override explicitly. - **No auto-merge**: The workflow cannot approve or merge PRs — it can only block via the merge gate. - **Concurrency controls**: Review jobs use `cancel-in-progress: true` to prevent stale reviews from posting. ### Verdict **FAIL** — The `Write` tool in the LLM `--allowedTools` list is overly broad and should be removed from all three Claude review jobs. This is straightforward to fix (delete `Write,` from the three `--allowedTools` strings). Once fixed, this workflow has solid security posture.
Author
Owner

Code Review

Blocking Issues

None.


Suggestions

  1. Field selector injection in event.ts and node.ts

    In event.ts, three methods build Kubernetes field selector strings by directly interpolating user-supplied arguments:

    // event.ts:170
    fieldSelector: `involvedObject.name=${args.podName},involvedObject.kind=Pod`
    // event.ts:198
    fieldSelector: `involvedObject.name=${args.deploymentName},involvedObject.kind=Deployment`
    // event.ts:225
    fieldSelector: `involvedObject.name=${args.serviceName},involvedObject.kind=Service`
    

    Similarly in node.ts:509:

    fieldSelector: `spec.nodeName=${args.nodeName}`
    

    If a caller passes a name containing , or = (e.g., "mypod,involvedObject.kind=Node"), the injected text is silently added to the field selector, overriding the intended kind filter or introducing unexpected constraints. Kubernetes resource names are supposed to follow DNS subdomain rules (no ,/=), but the code places no guard here.

    The blast radius is limited — worst case is empty or mis-filtered results within the caller's own cluster, not privilege escalation — so this is not blocking. A simple defensive check (if (/[,=]/.test(name)) throw new Error(...)) would close the gap.

  2. container argument in pod.ts exec is not validated

    The PR correctly adds pod-name validation at pod.ts:477 to prevent names starting with - from being interpreted as kubectl flags. The container parameter (used as -c <container>) has no corresponding validation. Because Deno.Command receives it as a discrete array element (not via shell), there is no injection risk, but the asymmetry is confusing to future readers. Adding a similar ^[a-z0-9][a-z0-9.-]*$ check (or just a "must not start with -" guard) for container would make the fix consistent.

  3. Pod name validation regex allows invalid trailing characters

    The regex at pod.ts:477 is ^[a-z0-9][a-z0-9\-.]*$. Kubernetes pod names must not end with - or . per DNS subdomain rules, but this regex permits it (e.g., "mypod-" is accepted). The security goal — preventing leading - — is fully achieved, but the looser-than-spec allowance could cause confusing errors from the Kubernetes API rather than a clean validation failure. Tightening to ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ would bring it in line with the spec.

  4. codegen-check CI matrix omits test — new snapshot tests not enforced

    codegen/shared/zodGenerator_test.ts is a new test file with 14 test cases (snapshot + unit). The codegen-check job matrix only includes [check, lint, fmt], so these tests are never executed in CI. The snapshot file (codegen/shared/__snapshots__/zodGenerator_test.ts.snap) is committed, so the tests would pass if run manually. Adding test to the codegen matrix (along with appropriate --allow-read --allow-write --allow-env flags) would ensure regressions are caught automatically.

## Code Review ### Blocking Issues None. --- ### Suggestions 1. **Field selector injection in `event.ts` and `node.ts`** In `event.ts`, three methods build Kubernetes field selector strings by directly interpolating user-supplied arguments: ```typescript // event.ts:170 fieldSelector: `involvedObject.name=${args.podName},involvedObject.kind=Pod` // event.ts:198 fieldSelector: `involvedObject.name=${args.deploymentName},involvedObject.kind=Deployment` // event.ts:225 fieldSelector: `involvedObject.name=${args.serviceName},involvedObject.kind=Service` ``` Similarly in `node.ts:509`: ```typescript fieldSelector: `spec.nodeName=${args.nodeName}` ``` If a caller passes a name containing `,` or `=` (e.g., `"mypod,involvedObject.kind=Node"`), the injected text is silently added to the field selector, overriding the intended `kind` filter or introducing unexpected constraints. Kubernetes resource names are supposed to follow DNS subdomain rules (no `,`/`=`), but the code places no guard here. The blast radius is limited — worst case is empty or mis-filtered results within the caller's own cluster, not privilege escalation — so this is not blocking. A simple defensive check (`if (/[,=]/.test(name)) throw new Error(...)`) would close the gap. 2. **`container` argument in `pod.ts` exec is not validated** The PR correctly adds pod-name validation at `pod.ts:477` to prevent names starting with `-` from being interpreted as kubectl flags. The `container` parameter (used as `-c <container>`) has no corresponding validation. Because `Deno.Command` receives it as a discrete array element (not via shell), there is no injection risk, but the asymmetry is confusing to future readers. Adding a similar `^[a-z0-9][a-z0-9.-]*$` check (or just a "must not start with `-`" guard) for `container` would make the fix consistent. 3. **Pod name validation regex allows invalid trailing characters** The regex at `pod.ts:477` is `^[a-z0-9][a-z0-9\-.]*$`. Kubernetes pod names must not end with `-` or `.` per DNS subdomain rules, but this regex permits it (e.g., `"mypod-"` is accepted). The security goal — preventing leading `-` — is fully achieved, but the looser-than-spec allowance could cause confusing errors from the Kubernetes API rather than a clean validation failure. Tightening to `^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$` would bring it in line with the spec. 4. **`codegen-check` CI matrix omits `test` — new snapshot tests not enforced** `codegen/shared/zodGenerator_test.ts` is a new test file with 14 test cases (snapshot + unit). The `codegen-check` job matrix only includes `[check, lint, fmt]`, so these tests are never executed in CI. The snapshot file (`codegen/shared/__snapshots__/zodGenerator_test.ts.snap`) is committed, so the tests would pass if run manually. Adding `test` to the codegen matrix (along with appropriate `--allow-read --allow-write --allow-env` flags) would ensure regressions are caught automatically.
Author
Owner

Adversarial Review

Critical / High

No critical or high severity findings.

Medium

  1. Codegen: bare {type: "object"} in resource schema now emits z.string() instead of z.record(z.string(), z.unknown())codegen/shared/zodGenerator.ts:390

    generateSimplifiedZodForProperty (used exclusively for resource schemas — line 107) now returns z.string() for objects with no properties, patternProperties, or additionalProperties. Previously it returned z.record(z.string(), z.unknown()).

    Breaking scenario: If any CloudFormation resource has a property typed as {type: "object"} with no sub-schema, and the cloud provider returns it as an actual JSON object in the resource state, the generated resource schema will reject it at runtime — z.string() cannot validate {"key": "value"}.

    Example: A hypothetical CF property Metadata: {type: "object"} where CloudControl returns {"Metadata": {"foo": "bar"}}. The old schema (z.record) would accept it; the new schema (z.string()) would reject it with a Zod validation error.

    Mitigating factor: Most freeform JSON fields in CloudFormation schemas have additionalProperties defined (which hits line 354-357 and still emits z.record) or are typed as json (line 322-325, correctly handled). Truly bare {type: "object"} with nothing else may be rare. However, the existing test (test 15) was specifically written to cover this case, suggesting it was a known concern.

    Test coverage deleted: Test 15 ("bare object with no properties emits z.record") was removed without a replacement test for the new behavior (zodGenerator_test.ts, lines 437-475 deleted). The new z.string() behavior for bare objects has zero test coverage. At minimum, a test asserting the new behavior should be added.

    Same change in generateObjectZod (zodGenerator.ts:540) affects the input schema path identically.

    Suggested fix: Either (a) restore z.record(z.string(), z.unknown()) for bare objects in the resource schema path (line 390), or (b) add a test proving this case doesn't occur with real schemas and document why z.string() is correct. For the input schema path (line 540), z.string() may be intentional if CloudControl accepts JSON-as-string — but it should still have a test.

Low

  1. Pod name regex allows . but Kubernetes pod names are DNS labelskubernetes/extensions/models/pod.ts:477

    The regex /^[a-z0-9][a-z0-9\-.]*$/ allows dots, but Kubernetes pod names must conform to RFC 1123 DNS labels: [a-z0-9]([-a-z0-9]*[a-z0-9])? (max 63 chars, no dots). Dots are only valid in DNS subdomain names (max 253 chars), used for some other Kubernetes resource names but not pods.

    This is overly permissive, not a security issue — a pod name with dots would simply fail kubectl lookup. But tightening the regex to match actual pod name rules would provide better error messages (fail at validation, not at kubectl).

    Suggested fix: /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ and optionally .max(63) on the Zod schema.

  2. CI: Write tool added to review agent allowed tools without path restriction.forgejo/workflows/ci.yml:730,845,936

    The Write tool was added alongside Read and Grep in the --allowedTools for all three review agents. Unlike the Bash(tee ...) which is path-scoped to ${OUTPUT_DIR}/review-body.md, Write is unrestricted — the review agent can write to any file in the checkout. Since the agent processes untrusted PR content, a prompt injection in the PR diff could attempt to get the agent to write files.

    Mitigating factor: The CI runner is ephemeral and the review agent's system prompt explicitly warns against following instructions in PR content. The blast radius is limited to the CI workspace.

    This is not blocking, but the principle of least privilege suggests restricting Write to the output directory if the tool supports path scoping.

Verdict

PASS — The security fix in pod.ts is correct and well-structured (array-based Deno.Command avoids shell injection, -- separator protects the command array, pod name validation prevents flag injection). The kubernetes version bumps and upgrade stubs are mechanical and correct. The codegen change to bare-object handling warrants attention (medium finding #1) but is mitigated by the rarity of truly bare {type: "object"} schemas and should be verified against real generated output before merge.

## Adversarial Review ### Critical / High No critical or high severity findings. ### Medium 1. **Codegen: bare `{type: "object"}` in resource schema now emits `z.string()` instead of `z.record(z.string(), z.unknown())`** — `codegen/shared/zodGenerator.ts:390` `generateSimplifiedZodForProperty` (used exclusively for **resource schemas** — line 107) now returns `z.string()` for objects with no `properties`, `patternProperties`, or `additionalProperties`. Previously it returned `z.record(z.string(), z.unknown())`. **Breaking scenario:** If any CloudFormation resource has a property typed as `{type: "object"}` with no sub-schema, and the cloud provider returns it as an actual JSON object in the resource state, the generated resource schema will reject it at runtime — `z.string()` cannot validate `{"key": "value"}`. Example: A hypothetical CF property `Metadata: {type: "object"}` where CloudControl returns `{"Metadata": {"foo": "bar"}}`. The old schema (`z.record`) would accept it; the new schema (`z.string()`) would reject it with a Zod validation error. **Mitigating factor:** Most freeform JSON fields in CloudFormation schemas have `additionalProperties` defined (which hits line 354-357 and still emits `z.record`) or are typed as `json` (line 322-325, correctly handled). Truly bare `{type: "object"}` with nothing else may be rare. However, the existing test (test 15) was specifically written to cover this case, suggesting it was a known concern. **Test coverage deleted:** Test 15 (`"bare object with no properties emits z.record"`) was removed without a replacement test for the new behavior (`zodGenerator_test.ts`, lines 437-475 deleted). The new `z.string()` behavior for bare objects has zero test coverage. At minimum, a test asserting the new behavior should be added. **Same change in `generateObjectZod`** (`zodGenerator.ts:540`) affects the input schema path identically. **Suggested fix:** Either (a) restore `z.record(z.string(), z.unknown())` for bare objects in the resource schema path (line 390), or (b) add a test proving this case doesn't occur with real schemas and document why `z.string()` is correct. For the input schema path (line 540), `z.string()` may be intentional if CloudControl accepts JSON-as-string — but it should still have a test. ### Low 2. **Pod name regex allows `.` but Kubernetes pod names are DNS labels** — `kubernetes/extensions/models/pod.ts:477` The regex `/^[a-z0-9][a-z0-9\-.]*$/` allows dots, but Kubernetes pod names must conform to RFC 1123 DNS labels: `[a-z0-9]([-a-z0-9]*[a-z0-9])?` (max 63 chars, no dots). Dots are only valid in DNS *subdomain* names (max 253 chars), used for some other Kubernetes resource names but not pods. This is overly permissive, not a security issue — a pod name with dots would simply fail kubectl lookup. But tightening the regex to match actual pod name rules would provide better error messages (fail at validation, not at kubectl). **Suggested fix:** `/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/` and optionally `.max(63)` on the Zod schema. 3. **CI: `Write` tool added to review agent allowed tools without path restriction** — `.forgejo/workflows/ci.yml:730,845,936` The `Write` tool was added alongside `Read` and `Grep` in the `--allowedTools` for all three review agents. Unlike the `Bash(tee ...)` which is path-scoped to `${OUTPUT_DIR}/review-body.md`, `Write` is unrestricted — the review agent can write to any file in the checkout. Since the agent processes untrusted PR content, a prompt injection in the PR diff could attempt to get the agent to write files. **Mitigating factor:** The CI runner is ephemeral and the review agent's system prompt explicitly warns against following instructions in PR content. The blast radius is limited to the CI workspace. This is not blocking, but the principle of least privilege suggests restricting `Write` to the output directory if the tool supports path scoping. ### Verdict **PASS** — The security fix in `pod.ts` is correct and well-structured (array-based `Deno.Command` avoids shell injection, `--` separator protects the command array, pod name validation prevents flag injection). The kubernetes version bumps and upgrade stubs are mechanical and correct. The codegen change to bare-object handling warrants attention (medium finding #1) but is mitigated by the rarity of truly bare `{type: "object"}` schemas and should be verified against real generated output before merge.
fix(ci): harden review prompts to use tee instead of Write tool
Some checks failed
CI / workflows/s3-bootstrap - fmt (pull_request) Has been skipped
CI / kubernetes - lockfile up to date (pull_request) Successful in 22s
CI / workflows/gcs-bootstrap - lint (pull_request) Has been skipped
CI / workflows/s3-bootstrap - lint (pull_request) Has been skipped
CI / workflows/s3-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / workflows/gcs-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / workflows/s3-bootstrap - test (pull_request) Has been skipped
CI / workflows/gcs-bootstrap - test (pull_request) Has been skipped
CI / cve/dirtyfrag - check (pull_request) Has been skipped
CI / cve/dirtyfrag - fmt (pull_request) Has been skipped
CI / cve/dirtyfrag - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - test (pull_request) Has been skipped
CI / cve/mini-shai-hulud - fmt (pull_request) Has been skipped
CI / cve/mini-shai-hulud - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 4m20s
CI / CI Security Review (pull_request) Has been skipped
CI / Claude Code Review (pull_request) Successful in 4m1s
CI / Adversarial Code Review (pull_request) Failing after 7m11s
CI / Merge Gate (pull_request) Failing after 2s
722db19ef1
The Write tool cannot be path-scoped in --allowedTools, so adding it
would give the review agent unrestricted filesystem write access —
a prompt injection vector. Instead, strengthen all three review prompts
to explicitly forbid Write and direct the model to use the already-scoped
tee command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Adversarial Review

High

  1. codegen/shared/zodGenerator.ts:196, 390, 539 — Reverts a deliberate bug fix, reintroducing the exact bug from issue #457

    This PR changes three fallback paths from z.record(z.string(), z.unknown()) back to z.string(). This is a direct revert of commit a3b6cfdd ("fix(codegen): emit z.record for freeform-JSON properties in input schemas (#457)"), which was specifically merged to fix a production failure: CloudControl rejects String for bare-object properties like PolicyDocument — only JSONObject is accepted.

    The previous fix's commit message explicitly documented:

    • "CloudControl expects JSONObject on writes, so string input passed swamp validation but failed at the AWS API."
    • "After this fix: JSON strings are correctly rejected at validation (expected record, received string) before reaching AWS"

    Breaking example: A user sets PolicyDocument on AWS::IAM::RolePolicy as a YAML object. With z.string(), Zod validation rejects the object (expected string, received object), so the user cannot create the resource — the exact bug from issue #457.

    Three affected paths:

    • Line 196: generateZodForProperty case "json" — input schema for JSON-typed CF properties
    • Line 539: generateObjectZod bare-object fallback — input schema for type "object" with no sub-properties (the PRIMARY path for PolicyDocument)
    • Line 390: generateSimplifiedZodForProperty bare-object fallback — resource schema; z.string() will reject actual JSON objects returned by AWS CloudControl GetResource, potentially causing read failures

    Impact scope: The previous fix affected 390 model files (182 properties across ~96 AWS services, ~39 GCP services). This revert affects the same scope.

    Suggested fix: Do not revert these three lines. Keep z.record(z.string(), z.unknown()) as established by the fix in a3b6cfdd.

  2. codegen/shared/zodGenerator_test.ts:401-433 — Test 15 deleted, test 14 asserts the opposite of its previous assertion

    Test 15 ("bare object with no properties emits z.record") was specifically added by commit a3b6cfdd to prevent regression. Deleting it removes the safety net.

    Test 14 was changed from asserting z.record(z.string(), z.unknown()) for JSON-type input schemas to asserting z.string() — directly contradicting the previous test's documented rationale ("CloudControl expects JSONObject, not String").

    Suggested fix: Restore test 15 and the original assertions in test 14.

Medium

  1. kubernetes/extensions/models/pod.ts:477 — container argument not validated in exec method

    The PR adds pod name validation to prevent kubectl flag injection, but the container field (line 493-494) has no equivalent validation. While Deno.Command array-style arg passing and the -c flag prefix make exploitation harder than the positional podName, the inconsistency is worth addressing for defense-in-depth. A container name like a whitespace string " " would pass the truthy check and be forwarded to kubectl.

    Suggested fix: Add the same regex validation for args.container when it is provided.

Low

  1. kubernetes/extensions/models/pod.ts:477 — Pod name regex allows trailing hyphens and dots

    The regex allows names like "my-pod-" or "my-pod.", which are invalid per RFC 1123 (labels must end with an alphanumeric character). Kubernetes itself would reject these, so this is not a security issue — just an overly permissive validation that could produce confusing error messages (Kubernetes error instead of the clearer custom error).

Verdict

FAIL — The codegen changes revert a deliberate, well-documented bug fix (commit a3b6cfdd / issue #457), reintroducing validation failures for freeform-JSON properties across ~96 AWS and ~39 GCP services. The Kubernetes security fix in pod.ts is correct and valuable but should be separated from the codegen regression.

## Adversarial Review ### High 1. **codegen/shared/zodGenerator.ts:196, 390, 539 — Reverts a deliberate bug fix, reintroducing the exact bug from issue #457** This PR changes three fallback paths from z.record(z.string(), z.unknown()) back to z.string(). This is a direct revert of commit a3b6cfdd ("fix(codegen): emit z.record for freeform-JSON properties in input schemas (#457)"), which was specifically merged to fix a production failure: CloudControl rejects String for bare-object properties like PolicyDocument — only JSONObject is accepted. The previous fix's commit message explicitly documented: - "CloudControl expects JSONObject on writes, so string input passed swamp validation but failed at the AWS API." - "After this fix: JSON strings are correctly rejected at validation (expected record, received string) before reaching AWS" **Breaking example:** A user sets PolicyDocument on AWS::IAM::RolePolicy as a YAML object. With z.string(), Zod validation rejects the object (expected string, received object), so the user cannot create the resource — the exact bug from issue #457. **Three affected paths:** - Line 196: generateZodForProperty case "json" — input schema for JSON-typed CF properties - Line 539: generateObjectZod bare-object fallback — input schema for type "object" with no sub-properties (the PRIMARY path for PolicyDocument) - Line 390: generateSimplifiedZodForProperty bare-object fallback — resource schema; z.string() will reject actual JSON objects returned by AWS CloudControl GetResource, potentially causing read failures **Impact scope:** The previous fix affected 390 model files (182 properties across ~96 AWS services, ~39 GCP services). This revert affects the same scope. **Suggested fix:** Do not revert these three lines. Keep z.record(z.string(), z.unknown()) as established by the fix in a3b6cfdd. 2. **codegen/shared/zodGenerator_test.ts:401-433 — Test 15 deleted, test 14 asserts the opposite of its previous assertion** Test 15 ("bare object with no properties emits z.record") was specifically added by commit a3b6cfdd to prevent regression. Deleting it removes the safety net. Test 14 was changed from asserting z.record(z.string(), z.unknown()) for JSON-type input schemas to asserting z.string() — directly contradicting the previous test's documented rationale ("CloudControl expects JSONObject, not String"). **Suggested fix:** Restore test 15 and the original assertions in test 14. ### Medium 1. **kubernetes/extensions/models/pod.ts:477 — container argument not validated in exec method** The PR adds pod name validation to prevent kubectl flag injection, but the container field (line 493-494) has no equivalent validation. While Deno.Command array-style arg passing and the -c flag prefix make exploitation harder than the positional podName, the inconsistency is worth addressing for defense-in-depth. A container name like a whitespace string " " would pass the truthy check and be forwarded to kubectl. **Suggested fix:** Add the same regex validation for args.container when it is provided. ### Low 1. **kubernetes/extensions/models/pod.ts:477 — Pod name regex allows trailing hyphens and dots** The regex allows names like "my-pod-" or "my-pod.", which are invalid per RFC 1123 (labels must end with an alphanumeric character). Kubernetes itself would reject these, so this is not a security issue — just an overly permissive validation that could produce confusing error messages (Kubernetes error instead of the clearer custom error). ### Verdict **FAIL** — The codegen changes revert a deliberate, well-documented bug fix (commit a3b6cfdd / issue #457), reintroducing validation failures for freeform-JSON properties across ~96 AWS and ~39 GCP services. The Kubernetes security fix in pod.ts is correct and valuable but should be separated from the codegen regression.
fix(ci): harden review sandbox with --add-dir and --disallowedTools
All checks were successful
CI / workflows/s3-bootstrap - lockfile up to date (pull_request) Has been skipped
CI / cve/dirtyfrag - check (pull_request) Has been skipped
CI / cve/dirtyfrag - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - test (pull_request) Has been skipped
CI / cve/mini-shai-hulud - check (pull_request) Has been skipped
CI / cve/dirtyfrag - fmt (pull_request) Has been skipped
CI / cve/mini-shai-hulud - fmt (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / aws models - sample check (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / gcp models - sample check (pull_request) Has been skipped
CI / cloudflare models - sample check (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 4m39s
CI / CI Security Review (pull_request) Successful in 2m32s
CI / Claude Code Review (pull_request) Successful in 4m5s
CI / Adversarial Code Review (pull_request) Successful in 6m12s
CI / Merge Gate (pull_request) Successful in 2s
7157dab69f
- Add --add-dir "$OUTPUT_DIR" to grant sandbox access to .review-output/
- Add --disallowedTools "Write" "Edit" to force tee usage (prevents
  prompt injection from bypassing scoped Bash commands)
- Update review prompts to explicitly forbid the Write tool

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Analysis

This PR hardens the Claude Code review sandbox across three review jobs (claude-review, claude-adversarial-review, claude-ci-security-review) in .forgejo/workflows/ci.yml by adding:

  • --add-dir "$OUTPUT_DIR" — scopes the agent's filesystem access to the review output directory
  • --disallowedTools "Write" "Edit" — explicitly blocks file-modification tools, forcing the agent to use only tee via the already-scoped Bash allowlist

The three prompt files (.forgejo/prompts/review.md, ci-security.md, adversarial.md) are updated to reinforce that the Write tool is unavailable, reducing the chance of the agent attempting disallowed tool calls.

Full-file review of ci.yml against the security checklist:

  1. Prompt Injection: No attacker-controlled event data (PR title, body, comments) is interpolated into LLM prompts. Changed file paths from git diff are included, which is a minimal and acceptable surface. All three prompts contain security preambles instructing the model to treat PR content as untrusted. Tool scoping is tight — only Read, Grep, Bash(git diff:*), Bash(git log:*), and two specific Bash(tee ...) / Bash(touch ...) patterns are allowed. No broad Bash(curl:*) or unrestricted Bash access.

  2. Expression Injection: Matrix values (matrix.task, matrix.vault, etc.) used in run: blocks are all hardcoded in the workflow — not attacker-controlled. SHAs (github.event.pull_request.base.sha, head.sha) are passed via env: variables (lines 40–41), which is the safe pattern. github.event.pull_request.number is also passed via env var. The merge-gate contains() expression (line 1015) produces only a boolean — safe.

  3. Dangerous Triggers: Uses pull_request trigger only (not pull_request_target). No issue_comment or workflow_dispatch triggers. Safe.

  4. Supply Chain: actions/checkout@v6 and denoland/setup-deno@v2 are from trusted publishers with tag pins — acceptable per policy. The Claude CLI binary is downloaded via curl but verified with a SHA-256 checksum before execution (lines 713–715, 829–831, 923–925). This is not a curl | bash pattern — it's a download-and-verify pattern, which is secure.

  5. Permissions: Workflow-level contents: read (line 9) applies to all non-review jobs, which is minimal and correct. The three Claude review jobs specify job-level permissions (contents: read + pull-requests: write), which is the minimum needed to post review comments. No id-token: write anywhere.

  6. Secret Exposure: ANTHROPIC_API_KEY is scoped to only the Claude invocation steps. BOT_TOKEN is scoped to only the comment-posting steps. Neither is logged or used in command substitution that could leak values.

  7. Auto-merge & Trust Boundaries: No auto-merge behavior. The workflow posts comments and optionally fails the job. The merge gate aggregates job results but does not merge anything.

Medium

(none)

Low

(none)

Verdict

PASS — This PR is a pure security hardening change. It adds --disallowedTools and --add-dir to constrain the Claude review agents, and updates prompts to match. No new vulnerabilities are introduced. The full workflow file passes all checklist items.

## CI Security Review ### Analysis This PR hardens the Claude Code review sandbox across three review jobs (`claude-review`, `claude-adversarial-review`, `claude-ci-security-review`) in `.forgejo/workflows/ci.yml` by adding: - `--add-dir "$OUTPUT_DIR"` — scopes the agent's filesystem access to the review output directory - `--disallowedTools "Write" "Edit"` — explicitly blocks file-modification tools, forcing the agent to use only `tee` via the already-scoped `Bash` allowlist The three prompt files (`.forgejo/prompts/review.md`, `ci-security.md`, `adversarial.md`) are updated to reinforce that the Write tool is unavailable, reducing the chance of the agent attempting disallowed tool calls. **Full-file review of `ci.yml` against the security checklist:** 1. **Prompt Injection**: No attacker-controlled event data (PR title, body, comments) is interpolated into LLM prompts. Changed file paths from `git diff` are included, which is a minimal and acceptable surface. All three prompts contain security preambles instructing the model to treat PR content as untrusted. Tool scoping is tight — only `Read`, `Grep`, `Bash(git diff:*)`, `Bash(git log:*)`, and two specific `Bash(tee ...)` / `Bash(touch ...)` patterns are allowed. No broad `Bash(curl:*)` or unrestricted Bash access. 2. **Expression Injection**: Matrix values (`matrix.task`, `matrix.vault`, etc.) used in `run:` blocks are all hardcoded in the workflow — not attacker-controlled. SHAs (`github.event.pull_request.base.sha`, `head.sha`) are passed via `env:` variables (lines 40–41), which is the safe pattern. `github.event.pull_request.number` is also passed via env var. The merge-gate `contains()` expression (line 1015) produces only a boolean — safe. 3. **Dangerous Triggers**: Uses `pull_request` trigger only (not `pull_request_target`). No `issue_comment` or `workflow_dispatch` triggers. Safe. 4. **Supply Chain**: `actions/checkout@v6` and `denoland/setup-deno@v2` are from trusted publishers with tag pins — acceptable per policy. The Claude CLI binary is downloaded via curl but verified with a SHA-256 checksum before execution (lines 713–715, 829–831, 923–925). This is not a `curl | bash` pattern — it's a download-and-verify pattern, which is secure. 5. **Permissions**: Workflow-level `contents: read` (line 9) applies to all non-review jobs, which is minimal and correct. The three Claude review jobs specify job-level permissions (`contents: read` + `pull-requests: write`), which is the minimum needed to post review comments. No `id-token: write` anywhere. 6. **Secret Exposure**: `ANTHROPIC_API_KEY` is scoped to only the Claude invocation steps. `BOT_TOKEN` is scoped to only the comment-posting steps. Neither is logged or used in command substitution that could leak values. 7. **Auto-merge & Trust Boundaries**: No auto-merge behavior. The workflow posts comments and optionally fails the job. The merge gate aggregates job results but does not merge anything. ### Medium *(none)* ### Low *(none)* ### Verdict **PASS** — This PR is a pure security hardening change. It adds `--disallowedTools` and `--add-dir` to constrain the Claude review agents, and updates prompts to match. No new vulnerabilities are introduced. The full workflow file passes all checklist items.
Author
Owner

Code Review

Blocking Issues

None.

Suggestions

  1. codegen/shared/zodGenerator_test.ts — snapshot files not committed
    Tests 1–12 use assertSnapshot, which requires snapshot files to exist. Those snapshot files do not appear in the changed-files list. If this file is newly added, deno test will fail on first run (Deno requires --update to generate snapshots). Because the codegen-check CI job only runs check, lint, and fmt — never test — this won't be caught by CI, but the tests themselves are non-functional without the snapshots. Either commit the snapshot files or confirm they already exist from a prior revision.

  2. kubernetes/extensions/models/event.ts (lines 170, 199, 228) — user input interpolated into fieldSelector string
    args.podName, args.deploymentName, and args.serviceName are interpolated directly into the Kubernetes field selector string:

    fieldSelector: `involvedObject.name=${args.podName},involvedObject.kind=Pod`,
    

    A value containing a comma (e.g. "nginx,involvedObject.kind=Warning") would silently add an extra selector clause and could return unexpected events. Since the namespace is already bound, this is information-disclosure at worst (not RCE), but it could produce surprising results for callers. Consider percent-encoding the value or validating that it contains no commas or = signs before interpolation.

  3. kubernetes/extensions/models/netpol.ts (lines 304–308) — double cast suggests type mismatch

    body: body as unknown as Parameters<
      typeof networkingApi.createNamespacedNetworkPolicy
    >[0]["body"],
    

    The as unknown as pattern bypasses the type system entirely. Either the spec: Record<string, unknown> shape doesn't match the Kubernetes client's expected type (pointing to a missing intermediate interface), or the cast could hide a subtle field mismatch. At minimum, annotate why the cast is necessary so a future reader knows it's intentional.

  4. kubernetes/extensions/models/deployment.ts (line 353) and pod.ts (line 295) — let body; without explicit type annotation
    In TypeScript strict mode, let body; without an initializer relies on control-flow inference for its type. If a future path is inserted between declaration and assignment, TypeScript may widen the type to any. Explicitly annotating these variables (e.g. as Record<string, unknown>) is more defensive and stays cleaner under the project's no-any rule.

  5. kubernetes/extensions/models/namespace.ts (line 917) — vacuous-truth health check
    An empty namespace (0 deployments, 0 pods, 0 services, 0 warnings) evaluates to healthy: true. This is likely the intended behaviour for an empty namespace, but it may surprise callers seeing a brand-new or fully-drained namespace marked healthy. A short comment explaining the intentional vacuous-truth behaviour would prevent future confusion.

## Code Review ### Blocking Issues None. ### Suggestions 1. **`codegen/shared/zodGenerator_test.ts` — snapshot files not committed** Tests 1–12 use `assertSnapshot`, which requires snapshot files to exist. Those snapshot files do not appear in the changed-files list. If this file is newly added, `deno test` will fail on first run (Deno requires `--update` to generate snapshots). Because the `codegen-check` CI job only runs `check`, `lint`, and `fmt` — never `test` — this won't be caught by CI, but the tests themselves are non-functional without the snapshots. Either commit the snapshot files or confirm they already exist from a prior revision. 2. **`kubernetes/extensions/models/event.ts` (lines 170, 199, 228) — user input interpolated into `fieldSelector` string** `args.podName`, `args.deploymentName`, and `args.serviceName` are interpolated directly into the Kubernetes field selector string: ```ts fieldSelector: `involvedObject.name=${args.podName},involvedObject.kind=Pod`, ``` A value containing a comma (e.g. `"nginx,involvedObject.kind=Warning"`) would silently add an extra selector clause and could return unexpected events. Since the namespace is already bound, this is information-disclosure at worst (not RCE), but it could produce surprising results for callers. Consider percent-encoding the value or validating that it contains no commas or `=` signs before interpolation. 3. **`kubernetes/extensions/models/netpol.ts` (lines 304–308) — double cast suggests type mismatch** ```ts body: body as unknown as Parameters< typeof networkingApi.createNamespacedNetworkPolicy >[0]["body"], ``` The `as unknown as` pattern bypasses the type system entirely. Either the `spec: Record<string, unknown>` shape doesn't match the Kubernetes client's expected type (pointing to a missing intermediate interface), or the cast could hide a subtle field mismatch. At minimum, annotate why the cast is necessary so a future reader knows it's intentional. 4. **`kubernetes/extensions/models/deployment.ts` (line 353) and `pod.ts` (line 295) — `let body;` without explicit type annotation** In TypeScript strict mode, `let body;` without an initializer relies on control-flow inference for its type. If a future path is inserted between declaration and assignment, TypeScript may widen the type to `any`. Explicitly annotating these variables (e.g. as `Record<string, unknown>`) is more defensive and stays cleaner under the project's no-`any` rule. 5. **`kubernetes/extensions/models/namespace.ts` (line 917) — vacuous-truth health check** An empty namespace (0 deployments, 0 pods, 0 services, 0 warnings) evaluates to `healthy: true`. This is likely the intended behaviour for an empty namespace, but it may surprise callers seeing a brand-new or fully-drained namespace marked healthy. A short comment explaining the intentional vacuous-truth behaviour would prevent future confusion.
Author
Owner

Adversarial Review

Medium

  1. Resource schema regression: bare object now emits z.string() instead of z.record() -- codegen/shared/zodGenerator.ts:390

    In generateSimplifiedZodForProperty (which generates resource state schemas -- the read/describe side), a bare type object with no properties, patternProperties, or additionalProperties now emits z.string(). Previously it emitted z.record(z.string(), z.unknown()).

    The design doc (codegen/designs/aws.md:686-688) explicitly states that the StateSchema should accept objects because AWS CloudControl returns these properties as already-deserialized objects. While the design doc discusses the json type (which is unchanged and still emits z.union), the same principle applies to bare objects in resource responses -- CloudControl returns them as actual JSON objects, not strings.

    Breaking example: If a CloudFormation schema has type object with no property definitions, and CloudControl returns a parsed object like a PolicyDocument, Zod validation will throw Expected string, received object.

    Mitigating factor: This is a narrow edge case -- most bare objects would have additionalProperties set (line 353-357 handles that and still emits z.record). The more common json type path is unaffected.

    Test coverage gap: Test 15 (bare object with no properties emits z.record) was deleted entirely. No replacement test verifies the new bare-object behavior in resource schemas. Even if z.string() is the intended behavior, the test deletion leaves this path uncovered.

    Suggested fix: Keep z.string() for the input schema paths (lines 196 and 540 -- these match the design doc), but restore z.record(z.string(), z.unknown()) at line 390 for the resource schema path. Add a test verifying the asymmetry.

  2. --disallowedTools syntax may only block Write, not Edit -- .forgejo/workflows/ci.yml:731 (also lines 848, 941)

    The flag is invoked as two separate shell words: --disallowedTools "Write" "Edit"

    But --allowedTools in the same command uses comma-separated format: --allowedTools "Read,Grep,Bash(...)"

    If --disallowedTools expects a single comma-separated argument (like --allowedTools), then Edit is a dangling positional argument and the Edit tool is NOT actually blocked. This would leave the review sandbox weaker than intended.

    Mitigating factor: The prompt text also instructs the model not to use Write/Edit, providing defense in depth. And if Claude Code CLI does accept multiple space-separated values for --disallowedTools, this works fine.

    Suggested fix: Use consistent syntax: --disallowedTools "Write,Edit" to match the comma-separated pattern of --allowedTools, or verify the CLI accepts the space-separated form.

Low

  1. Pod name regex allows trailing dash and dot -- kubernetes/extensions/models/pod.ts:477

    Regex: /^[a-z0-9][a-z0-9-.]*$/

    Kubernetes DNS subdomain names must start AND end with an alphanumeric character. This regex allows my-pod- or my-pod. which are invalid pod names. The primary purpose of this validation is preventing kubectl flag injection (names starting with dash), which it does correctly. kubectl would reject invalid trailing characters anyway.

    Suggested fix (optional): /^a-z0-9?$/ to also enforce the trailing character constraint.

  2. container argument not validated -- kubernetes/extensions/models/pod.ts:493-494

    args.container is passed directly to kubectlArgs.push without validation. A container name like --namespace=kube-system would be treated as a literal -c argument value by Deno.Command array-based execution (no shell involved), so this is not exploitable for flag injection. Still, applying the same pattern as pod name validation would be consistent defense-in-depth.

Verdict

PASS -- The pod exec security fix is sound and the CI hardening is a clear improvement. The codegen z.string() change is correct for input schemas (matching the design doc) but questionable for the resource schema bare-object path. The resource schema concern and the --disallowedTools syntax inconsistency are worth addressing but neither is blocking given their limited blast radius.

## Adversarial Review ### Medium 1. **Resource schema regression: bare object now emits z.string() instead of z.record()** -- codegen/shared/zodGenerator.ts:390 In generateSimplifiedZodForProperty (which generates resource state schemas -- the read/describe side), a bare type object with no properties, patternProperties, or additionalProperties now emits z.string(). Previously it emitted z.record(z.string(), z.unknown()). The design doc (codegen/designs/aws.md:686-688) explicitly states that the StateSchema should accept objects because AWS CloudControl returns these properties as already-deserialized objects. While the design doc discusses the json type (which is unchanged and still emits z.union), the same principle applies to bare objects in resource responses -- CloudControl returns them as actual JSON objects, not strings. Breaking example: If a CloudFormation schema has type object with no property definitions, and CloudControl returns a parsed object like a PolicyDocument, Zod validation will throw Expected string, received object. Mitigating factor: This is a narrow edge case -- most bare objects would have additionalProperties set (line 353-357 handles that and still emits z.record). The more common json type path is unaffected. Test coverage gap: Test 15 (bare object with no properties emits z.record) was deleted entirely. No replacement test verifies the new bare-object behavior in resource schemas. Even if z.string() is the intended behavior, the test deletion leaves this path uncovered. Suggested fix: Keep z.string() for the input schema paths (lines 196 and 540 -- these match the design doc), but restore z.record(z.string(), z.unknown()) at line 390 for the resource schema path. Add a test verifying the asymmetry. 2. **--disallowedTools syntax may only block Write, not Edit** -- .forgejo/workflows/ci.yml:731 (also lines 848, 941) The flag is invoked as two separate shell words: --disallowedTools "Write" "Edit" But --allowedTools in the same command uses comma-separated format: --allowedTools "Read,Grep,Bash(...)" If --disallowedTools expects a single comma-separated argument (like --allowedTools), then Edit is a dangling positional argument and the Edit tool is NOT actually blocked. This would leave the review sandbox weaker than intended. Mitigating factor: The prompt text also instructs the model not to use Write/Edit, providing defense in depth. And if Claude Code CLI does accept multiple space-separated values for --disallowedTools, this works fine. Suggested fix: Use consistent syntax: --disallowedTools "Write,Edit" to match the comma-separated pattern of --allowedTools, or verify the CLI accepts the space-separated form. ### Low 3. **Pod name regex allows trailing dash and dot** -- kubernetes/extensions/models/pod.ts:477 Regex: /^[a-z0-9][a-z0-9\-.]*$/ Kubernetes DNS subdomain names must start AND end with an alphanumeric character. This regex allows my-pod- or my-pod. which are invalid pod names. The primary purpose of this validation is preventing kubectl flag injection (names starting with dash), which it does correctly. kubectl would reject invalid trailing characters anyway. Suggested fix (optional): /^[a-z0-9]([a-z0-9\-.]*[a-z0-9])?$/ to also enforce the trailing character constraint. 4. **container argument not validated** -- kubernetes/extensions/models/pod.ts:493-494 args.container is passed directly to kubectlArgs.push without validation. A container name like --namespace=kube-system would be treated as a literal -c argument value by Deno.Command array-based execution (no shell involved), so this is not exploitable for flag injection. Still, applying the same pattern as pod name validation would be consistent defense-in-depth. ### Verdict **PASS** -- The pod exec security fix is sound and the CI hardening is a clear improvement. The codegen z.string() change is correct for input schemas (matching the design doc) but questionable for the resource schema bare-object path. The resource schema concern and the --disallowedTools syntax inconsistency are worth addressing but neither is blocking given their limited blast radius.
stack72 deleted branch fix/kubernetes-exec-flag-injection 2026-05-27 21:31:01 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
swamp-club/swamp-extensions!6
No description provided.