Skip to content

fix(bundler): quote recipe-supplied name and namespace in install.sh - #2853

Merged
mchmarny merged 2 commits into
mainfrom
fix/localformat-shell-quote-name-namespace
Sep 22, 2026
Merged

mchmarny merged 2 commits into
mainfrom
fix/localformat-shell-quote-name-namespace

Conversation

@lockwobr

Copy link
Copy Markdown
Contributor

Summary

Quote {{ .Name }} and {{ .Namespace }} where the localformat install templates interpolate them into the helm upgrade command line, using the shellSingleQuote helper the sibling apply-crds.sh template already uses.

Motivation / Context

install-upstream-helm.sh.tmpl and install-local-helm.sh.tmpl built their helm upgrade invocation with both values bare:

helm upgrade --install ${FORCE_CONFLICTS_FLAG} {{ .Name }} "${CHART_REF}" \
  --namespace {{ .Namespace }} --create-namespace \

Neither value is constrained to shell-safe characters on the way in. Component names are validated only as path components, and deployer.IsSafePathComponent rejects separators rather than shell metacharacters; the namespace is not validated on this path at all. The result is a generated script that an operator then runs, so a malformed or hostile recipe turns into command execution on the operator's workstation with their cluster credentials loaded.

This is defense in depth rather than a remotely reachable exploit: the values come from recipes/registry.yaml and recipe overlays, both of which are reviewed. But relying on a distant validator that was written for a different purpose, and that does not actually cover this character class, is the fragile part worth removing.

Fixes: #2844
Related: N/A

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Build/CI/tooling

Component(s) Affected

  • CLI (cmd/aicr, pkg/cli)
  • API server (cmd/aicrd, pkg/server)
  • Recipe engine / data (pkg/recipe)
  • Bundlers (pkg/bundler, pkg/component/*)
  • Collectors / snapshotter (pkg/collector, pkg/snapshotter)
  • Validator (pkg/validator)
  • Core libraries (pkg/errors, pkg/k8s)
  • Docs/examples (docs/, examples/)
  • Other: ____________

Implementation Notes

No new escaping mechanism. shellSingleQuote already existed in upstream_helm.go and was already registered as shq on the apply-crds.sh template; these two templates simply never adopted it. Single quotes rather than double are what matters here, because $(), backticks, and $VAR all still expand inside double quotes.

The template.FuncMap{"shq": shellSingleQuote} literal would have appeared three times, which goconst flags, so it is hoisted into one shellFuncs var carrying the rationale in its doc comment.

16 golden install.sh fixtures and stock_render_golden.yaml are regenerated; those are the only other changes.

Testing

go test -race ./pkg/bundler/deployer/...
go test ./pkg/bundler/ -run TestStockRenderParityGolden
golangci-lint run -c .golangci.yaml ./pkg/bundler/deployer/...

All pass; linter reports 0 issues.

The regression is execution-based, and mutation-verified. A strings.Contains assertion on the rendered script would prove nothing about quoting, since it passes whether or not the value is safe to hand to bash. Instead each test renders a bundle whose name and namespace embed both a space and a $(touch <canary>) substitution, runs the script under a stub helm that records its argv, and asserts the hostile value arrived as a single argv element and that no canary file exists.

Reverting just the | shq filters and re-running shows the payload genuinely executing, not merely word-splitting:

--- FAIL: TestWrite_UpstreamHelmInstallScript_RejectsInjectedRecipeValues
    helm upgrade did not receive "nfd evil$(touch pwned-upstream-name)" as a single argv element.
    injected command executed: .../001-nfd evil$(touch pwned-upstream-name)/pwned-upstream-name exists
--- FAIL: TestWrite_LocalHelmInstallScript_RejectsInjectedRecipeValues
    helm upgrade did not receive "skyhook evil$(touch pwned-local-name)" as a single argv element.

Coverage: pkg/bundler/deployer/localformat 82.2%. No exported API added.

Risk Assessment

  • Low — Isolated change, well-tested, easy to revert
  • Medium — Touches multiple components or has broader impact
  • High — Breaking change, affects critical paths, or complex rollout

Two template lines plus a FuncMap hoist. Every currently-shipped component name and namespace is a plain DNS-1123 label, for which single-quoting is a no-op in effect, so the rendered behavior is unchanged for real recipes. The regenerated goldens show exactly that: quotes appear, nothing else moves.

Rollout notes: N/A.

Checklist

  • Tests pass locally (make test with -race)
  • Linter passes (make lint)
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality
  • I updated docs if user-facing behavior changed
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S)

@lockwobr lockwobr added the theme/deployer Helm, ArgoCD, and deployment bundle generation label Sep 19, 2026
@lockwobr lockwobr self-assigned this Sep 19, 2026
@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: NVIDIA/aicr/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 9af686ac-f35a-4cb5-8b10-6c862291eb79

📥 Commits

Reviewing files that changed from the base of the PR and between 60502d3 and 3d94b35.

📒 Files selected for processing (1)
  • pkg/bundler/testdata/stock_render_golden.yaml

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


📝 Walkthrough

Walkthrough

The change applies the shared shq template function to Helm release names and namespaces in local and upstream installer templates. Generated installer fixtures now contain quoted arguments. Writer tests execute generated scripts with hostile values, verify exact Helm arguments, and confirm command substitutions are not executed. Existing Helm options and --create-namespace behavior remain unchanged.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: ayuskauskas

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: shell-quoting recipe-supplied names and namespaces in install.sh.
Description check ✅ Passed The description directly explains the shell-injection risk, implementation, affected templates, regression tests, and scope. It is fully related to the changeset.
Linked Issues check ✅ Passed Issue #2844 requires shell quoting for .Name and .Namespace in both localformat Helm install templates. Both templates now use the existing shq helper. The shared shellFuncs map registers the …
Out of Scope Changes check ✅ Passed The changes remain within Issue #2844. The shared function map, parser update, golden fixtures, and execution tests support the quoting fix. The changes do not modify the already-safe CRD behavior, YA…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@lockwobr
lockwobr force-pushed the fix/localformat-shell-quote-name-namespace branch from 566bf81 to ffa53bc Compare September 19, 2026 02:16
@lockwobr
lockwobr marked this pull request as ready for review September 19, 2026 02:33
@lockwobr
lockwobr requested a review from a team as a code owner September 19, 2026 02:33
@lockwobr
lockwobr force-pushed the fix/localformat-shell-quote-name-namespace branch from ffa53bc to fea8500 Compare September 19, 2026 02:44
@github-actions

Copy link
Copy Markdown
Contributor

Recipe evidence check

No leaf overlays affected by this PR.

This gate is warning-only and never blocks merge.

@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report ✅

Metric Value
Coverage 84.9%
Threshold 83%
Status Pass
Coverage Badge
![Coverage](https://img.shields.io/badge/coverage-84.9%25-brightgreen)

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/NVIDIA/aicr/pkg/bundler/deployer/localformat 82.19% (+0.27%) 👍

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/NVIDIA/aicr/pkg/bundler/deployer/localformat/local_helm.go 85.87% (-0.18%) 92 (+6) 79 (+5) 13 (+1) 👎
github.com/NVIDIA/aicr/pkg/bundler/deployer/localformat/upstream_helm.go 77.78% (+14.14%) 36 (+14) 28 (+14) 8 🎉

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

@lockwobr
lockwobr force-pushed the fix/localformat-shell-quote-name-namespace branch 3 times, most recently from 3d94b35 to 43be8f0 Compare September 20, 2026 04:39
mchmarny
mchmarny previously approved these changes Sep 20, 2026

@mchmarny mchmarny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve: no findings against 43be8f0. Focused quoting tests pass; the failed NVSentinel E2E is an unrelated monitor query/timing race at the reviewed SHA.

install-upstream-helm.sh.tmpl and install-local-helm.sh.tmpl interpolated
{{ .Name }} and {{ .Namespace }} bare into the helm upgrade command line.
Component names are validated only as path components, which rejects
separators but not shell metacharacters, and the namespace is not
validated on this path at all, so a recipe could put arbitrary shell into
a script the operator then runs.

Apply the shellSingleQuote helper the sibling apply-crds.sh template
already uses, hoisted into a shared shellFuncs map.

Regression runs the rendered script with a payload embedding both a space
and a command substitution, and asserts helm received each value as one
argv element and that no canary file was created. Reverting the quoting
makes the payload execute.

Signed-off-by: Brian Lockwood <lockwobr@gmail.com>
@lockwobr
lockwobr force-pushed the fix/localformat-shell-quote-name-namespace branch from d17c667 to 8eba4e8 Compare September 22, 2026 15:52
@lockwobr

Copy link
Copy Markdown
Contributor Author

Heads up @mchmarny: I force-pushed over your d17c66756 ("Merge branch 'main' into fix/localformat-shell-quote-name-namespace"). The branch is now 8eba4e895, rebased onto 261234b56.

No content of yours was lost. I checked before saying so: the only commits unique to d17c66756 versus main were your merge commit itself and my 43be8f04c, and c61d8d25b had already landed on main independently. The rebased branch carries the same tree relative to current main, plus a regenerated stock_render_golden.yaml.

The end state matches what the repo asks for — CLAUDE.md requires rebasing rather than a merge commit or the "Update branch" button, since squash-merge composes the commit from the PR title. But that is a justification for the result, not for how I got there: my sync script read the remote head and leased against that value, which only proves nothing changed between the read and the push. It could not tell that what it read was your commit rather than mine. I have since changed it to refuse the push outright when the remote carries any commit the local branch does not contain.

Sorry for the churn. If you were mid-something on this branch, tell me and I will restore d17c66756 from reflog.

These two fixtures arrived on main after this branch was cut, so the
rebase brought in install.sh goldens that predate the quoting change.

Signed-off-by: Brian Lockwood <lockwobr@gmail.com>
@mchmarny
mchmarny merged commit 37be501 into main Sep 22, 2026
77 checks passed
@mchmarny
mchmarny deleted the fix/localformat-shell-quote-name-namespace branch September 22, 2026 16:43
ArangoGutierrez added a commit that referenced this pull request Sep 22, 2026
The two golden fixtures collided on derived values: every conflict hunk was a
sha256 of bundle-info.yaml or checksums.txt. Neither side's digest describes
the merged tree, so both goldens were regenerated with make update-goldens
rather than resolved by hand.

Verified the result carries both sides: it is byte-different from each pure
side, our slinky-slurm and topograph values digests are present, and main's
#2853 quoting shows in the rendered install.sh fixtures as 'gpu-operator'.
catalog_parity_golden.yaml auto-merged and regeneration left it unchanged,
which is the check that the automatic resolution was correct.

Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/bundler size/XL theme/deployer Helm, ArgoCD, and deployment bundle generation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(bundler): unquoted name/namespace in localformat install.sh templates

2 participants