Your Helm chart splices a values string into a container's sh -c command. What is the risk?
answer
- quoting is per parser, not global
- ask who parses this next
- valid YAML can carry a shell payload
- argv list instead of sh -c
- an unbounded annotation map is a control channel
basics
~20 sThe value crosses two parsers. quote makes it one safe YAML scalar, but that scalar is handed to a shell inside the container, where its metacharacters run as commands. Use an args list with no shell instead.
solid answer
~40 sQuoting is per-parser, and a shell command line is a second parser the template never sees. Rendering `sh -c` with an interpolated value produces a valid manifest whose payload carries whatever semicolons, backticks and command substitutions the caller supplied, and the container's shell executes them - with that pod's ServiceAccount, mounted Secrets and network position. YAML quoting cannot help, because YAML did its job correctly. The same shape appears wherever a value crosses into another interpreter: a spliced `toYaml` annotations map lets the caller set arbitrary annotation keys, including Helm's own `helm.sh/hook` or `helm.sh/resource-policy: keep`. The fixes are structural: drop `sh -c` and use `command` plus an `args` list so each element is one argument; pass values as environment variables rather than command text; and pin the field with a pattern in `values.schema.json`.
code
yaml · 13 lines# Fragile: the rendered text is parsed again by the container's shell
command:
- /bin/sh
- -c
- transcode --input {{ .Values.inputUrl | quote }} --out /data
---
# Safer: no shell, one argument per list element
command: ["transcode"]
args:
- "--input"
- {{ .Values.inputUrl | quote }}
- "--out"
- "/data"go deeper
Know that quote protects the YAML layer only. If the rendered string is then handed to a shell inside the container, that shell reads its metacharacters, and no amount of YAML quoting changes it.
Explain the two-parser sequence and the concrete fix: command plus an args list so each element is passed as one argument with no shell involved, and a schema pattern on fields that must have a known shape.
An interviewer expects the trust framing - on a shared chart the values author is not the chart author - plus the annotation variant, where a spliced map lets a caller set Helm's own control annotations, and the review greps you would run on an unfamiliar chart.
Own the chart-authoring standard: which fields platform charts accept, whether any chart may build a command line from values, how the input surface is declared in a schema, and what review a chart passes before others are told to install it.
## Two parsers, two quoting problems Every interpolation in a Helm chart has to survive the parser that reads it. `quote` handles exactly one of them - the YAML parser that turns rendered text into an object. The moment the rendered value is handed to something else that parses it again, YAML quoting is irrelevant, because YAML already did its job: it delivered the string faithfully, metacharacters and all, to the next consumer. The common case is a container command. A chart for a media-transcoding pipeline writes something like: ```yaml command: - /bin/sh - -c - transcode --input {{ .Values.inputUrl | quote }} --out /data ``` This renders valid YAML. It also renders a shell script whose text the caller partly controls. A value ending in a semicolon followed by another command produces a command line the container's shell splits and runs as two commands. The manifest is well-formed, `helm lint` is happy, a structural check sees nothing wrong, and the payload runs with that pod's ServiceAccount token, its mounted Secrets, and its network position inside the cluster. Note that `quote` here is not merely insufficient - it is a false comfort. It puts double quotes around the value in the *YAML*, which the YAML parser then removes, delivering the bare string into the shell argument. Nothing in Helm knows the string is destined for a shell. ## Why this is a chart-design problem, not a values problem It is tempting to answer "then do not pass hostile values". But the entire premise of a shared platform chart is that the chart author and the values author are different people. Values arrive from a tenant's repository, a pipeline variable, an application manifest a reconciler applies, or a colleague's `-f` file. A chart that turns any of those into shell text has made an input executable, and the trust boundary now sits inside the chart rather than at its edge. The structural fixes are the ones that hold: - **Do not run a shell.** `command: ["transcode"]` with `args:` as a list means each element is passed as one argv entry; nothing splits on spaces or semicolons because no shell is involved. Metacharacters in a value become literal characters in an argument, which is the correct outcome. - **Pass data as environment or a mounted file.** An `env` entry, or a config file the program reads, keeps the value off any command line, and a program reading its own config is not parsing shell. - **Constrain the field.** `values.schema.json` can require a pattern for a URL-shaped field or an enum for a mode flag. Helm validates supplied values against the schema on install, upgrade, lint and template, so an out-of-shape value fails before rendering rather than after applying. - **Keep a shell out of the image where you can.** If the container has no shell, an `sh -c` entrypoint fails loudly instead of running a payload - a useful side effect of minimal images. ## The annotation variant of the same bug The other place a value crosses into another interpreter is a metadata block a chart splices wholesale: ```yaml metadata: annotations: {{- toYaml .Values.extraAnnotations | nindent 4 }} ``` `toYaml` is doing the right thing structurally - it re-serialises the map, so there is no YAML break-out. The problem is the *key space*: the caller may set **any** annotation on that object, and annotations are an instruction channel that several readers act on. The sharpest examples belong to Helm itself. A caller who can set `helm.sh/hook` on a rendered resource converts that object into a hook - Helm then treats it as part of a hook phase rather than part of the release's manifest, so it is no longer tracked with the release. A caller who sets `helm.sh/resource-policy: keep` makes the object survive `helm uninstall`. Neither requires touching the chart. Beyond Helm, an annotation on a workload or a Service is how many in-cluster controllers are configured, so an unbounded annotations map is an unbounded configuration surface for whatever controllers happen to be installed. The fix mirrors the command case: do not splice an unbounded map into a place where keys carry semantics. Either accept a fixed set of fields the chart writes into named annotations, or filter the map to a documented key prefix, and declare the shape in the schema. ## How to find these in a chart you did not write Render it and read it. `helm template` with realistic values shows the exact shell string and the exact annotation set. Then grep the templates for `sh -c`, for a `command:` containing an interpolation, and for `toYaml` landing under `annotations:` or `labels:`. A 3.4 MB chart tarball is far too big to read line by line, but those three greps cover most of the surface, and they are worth writing down as the checklist for anyone on the team who installs charts from outside. ## The framing to keep Quoting is not a security control on its own - it is a per-parser encoding step. The question to ask at every interpolation is who parses this next. If the answer is YAML, `quote` is right. If the answer is a shell, a URL parser, a log processor or a controller reading an annotation key, then the correct control belongs to that consumer: an argv list, a validated field, a bounded key set. A chart that never builds a command line out of caller input has removed the class rather than escaping its way around it.
- A chart splices a caller's map under metadata.annotations with toYaml. What can the caller reach that the author did not intend?Any annotation key on that object. Two belong to Helm itself: `helm.sh/hook` reclassifies the object as a hook so it leaves the release's tracked manifest, and `helm.sh/resource-policy: keep` makes it survive uninstall. Beyond Helm, annotations configure whatever controllers are installed. Accept named fields, or filter to a documented prefix, instead of splicing an unbounded map.
- Where would you enforce the constraint - in the template or before rendering?Before rendering. `values.schema.json` with a pattern or an enum on the field is checked on install, upgrade, lint and template, so a bad value fails with a clear message and no manifest is produced. Template-side sanitising is easy to bypass by adding a second code path, and it puts the check in the least reviewed place in the chart.
- Would an admission policy engine in the cluster catch this?Only if someone wrote a rule for the shape, and that rule is awkward: the manifest is structurally valid, the image is approved, and the payload is one string inside an argument. Such an engine is a useful backstop for known-bad patterns like a shell entrypoint, but the reliable control is the chart not constructing command text from caller input in the first place.
Quoting for YAML and then handing the string to a shell is like sealing a letter correctly and posting it to someone who treats every sentence inside as an order.
saying these in an interview costs you the question
- Believes quote makes a value safe everywhere
- Thinks valid YAML implies a safe manifest
- Says helm lint would catch a shell payload
- Strips characters instead of avoiding a shell
- Treats toYaml on annotations as fully safe
- Assumes only the author can set Helm's own annotations