docs: warn about YAML injection in chart templates - #2211
Conversation
| | Goal | Prefer | Avoid | | ||
| | ---- | ------ | ----- | | ||
| | Quote a string scalar | `{{ .Values.name \| quote }}` | `{{ .Values.name }}` in a bare field | | ||
| | Indent a multi-line string | `{{ .Values.config \| nindent 4 }}` or `\| indent 4` after `toYaml` | Pasting multi-line values next to a key without indentation | |
There was a problem hiding this comment.
This {{ .Values.config \| nindent 4 }} is still injectable.
There was a problem hiding this comment.
I'd say this depends on the context. Imho this is perfectly secure, if this is used inside a YAML block whose indent is smaller than 4. See my new comment there...
|
Good catch — |
Signed-off-by: Dean Chen <862469039@qq.com>
|
you're right — bare |
6a67b7b to
cc617dc
Compare
|
@TerryHowe — rebased the table so bare nindent isn't listed as safe anymore (maps go through toYaml first, and there's a bad example calling out that nindent alone still injects). PTAL when you get a chance. |
paigecalvert
left a comment
There was a problem hiding this comment.
@locker95 a few comments. main issue was table rendering seems off!
| Because Helm and Kubernetes often read, modify, and then rewrite YAML files, the | ||
| anchors will be lost. | ||
|
|
||
| ## Safe embedding of values (YAML injection) |
There was a problem hiding this comment.
| ## Safe embedding of values (YAML injection) | |
| ## Prevent YAML injection when inserting Helm values |
There was a problem hiding this comment.
WDYT about this heading edit? Wanted to address a few things:
- change from passive to active voice
- "(YAML injection)" in parentheses felt a little vague. wanted to make it clear that it's something that you're trying to avoid
- "insert" felt like a plainer, more straightforward verb for this vs "embed". Open to leaving it as embed if that feels more accurate, though
| the rendered YAML (YAML injection). That can break installs—or worse, inject | ||
| extra keys into a Kubernetes manifest. |
There was a problem hiding this comment.
| the rendered YAML (YAML injection). That can break installs—or worse, inject | |
| extra keys into a Kubernetes manifest. | |
| the rendered YAML. This is called _YAML injection_. | |
| YAML injection can not only break installs, but can | |
| also inject extra keys into a Kubernetes manifest. |
| `nindent` only adds indentation; it does **not** encode YAML. Pair it with `toYaml` | ||
| for maps/lists, or use `quote` for a single scalar. Bare `nindent` on an | ||
| untrusted string is still injectable. |
There was a problem hiding this comment.
This is a good explainer, but it should probably be moved next to "toYaml + nindent (or chart helpers)" in the table, since I think it's specific to that row.
Alternatively, we could also add a Why? column so that there is a clear explanation of each recommendation.
| | Embed maps / lists | `{{ toYaml .Values.extraEnv | nindent 8 }}` | `{{ .Values.extraEnv | nindent 8 }}` without `toYaml`, or hand-rolled `key: {{ . }}` loops | | ||
| | Labels / annotations maps | `toYaml` + `nindent` (or chart helpers) | Concatenating free-form label lines from values | | ||
|
|
||
| `nindent` only adds indentation; it does **not** encode YAML. Pair it with `toYaml` |
There was a problem hiding this comment.
| `nindent` only adds indentation; it does **not** encode YAML. Pair it with `toYaml` | |
| `nindent` only adds indentation; it doesn't encode YAML. Pair it with `toYaml` |
(style edit: don't use bold text for emphasis)
| 3. Run `helm template` (and schema validation) in CI so malformed values fail | ||
| before they reach the cluster. | ||
|
|
||
| See also [Functions and Pipelines](functions_and_pipelines.mdx), |
There was a problem hiding this comment.
| See also [Functions and Pipelines](functions_and_pipelines.mdx), | |
| For more information, see [Template Functions and Pipelines](functions_and_pipelines.mdx), |
| before they reach the cluster. | ||
|
|
||
| See also [Functions and Pipelines](functions_and_pipelines.mdx), | ||
| [Indenting and Templates](#indenting-and-templates), and the |
There was a problem hiding this comment.
| [Indenting and Templates](#indenting-and-templates), and the | |
| [Indenting and Templates](#indenting-and-templates) on this page, and the |
|
|
||
| See also [Functions and Pipelines](functions_and_pipelines.mdx), | ||
| [Indenting and Templates](#indenting-and-templates), and the | ||
| [function list](function_list.mdx) (`toYaml`, `quote`, `nindent`). |
There was a problem hiding this comment.
| [function list](function_list.mdx) (`toYaml`, `quote`, `nindent`). | |
| [Template Function List](function_list.mdx) (`toYaml`, `quote`, `nindent`). |
| When you embed chart values into manifests, also read | ||
| [Safe embedding of values (YAML injection)](#safe-embedding-of-values-yaml-injection) |
There was a problem hiding this comment.
note: if we change the section heading below this needs to be updated accordingly
Signed-off-by: Dean Chen <862469039@qq.com>
|
escaped the table pipes so it actually renders, moved the nindent note into that row, and took your heading/wording/link edits. |
paigecalvert
left a comment
There was a problem hiding this comment.
thank you @locker95 ! I did one more small pass of edits; just copyedits and also shuffled the info around a bit so that all the "best practices" guidance was grouped together.
| YAML injection can not only break installs, but can | ||
| also inject extra keys into a Kubernetes manifest. | ||
|
|
||
| Prefer helpers that encode or structure the data for you: |
There was a problem hiding this comment.
| Prefer helpers that encode or structure the data for you: | |
| ### Best practices | |
| To avoid YAML injection when inserting Helm values, | |
| prefer helpers that encode or structure the data for you, as shown in the following table: |
| | Multi-line free-form string | Store structured data as a map/list and use `toYaml`, or treat the whole value as one quoted scalar with `quote` | `{{ .Values.config \| nindent 4 }}` — `nindent` only indents; newline-bearing values can still inject keys | | ||
| | Embed maps / lists | `{{ toYaml .Values.extraEnv \| nindent 8 }}`. `nindent` only adds indentation; it doesn't encode YAML. Pair it with `toYaml` for maps/lists, or use `quote` for a single scalar. Bare `nindent` on an untrusted string is still injectable. | `{{ .Values.extraEnv \| nindent 8 }}` without `toYaml`, or hand-rolled `key: {{ . }}` loops | | ||
| | Labels / annotations maps | `toYaml` + `nindent` (or chart helpers) | Concatenating free-form label lines from values | | ||
|
|
There was a problem hiding this comment.
| The following describes general best practices for preventing YAML injection when inserting values: | |
| - Treat every `{{ ... }}` expression that is rendered inside YAML as untrusted input. | |
| - Use `quote`, `toYaml`, and `nindent`/`indent` together to preserve YAML structure. Avoid manually constructing YAML using string concatenation. | |
| - Run `helm template` and schema validation in CI so malformed values fail | |
| before they reach the cluster. |
| | Embed maps / lists | `{{ toYaml .Values.extraEnv \| nindent 8 }}`. `nindent` only adds indentation; it doesn't encode YAML. Pair it with `toYaml` for maps/lists, or use `quote` for a single scalar. Bare `nindent` on an untrusted string is still injectable. | `{{ .Values.extraEnv \| nindent 8 }}` without `toYaml`, or hand-rolled `key: {{ . }}` loops | | ||
| | Labels / annotations maps | `toYaml` + `nindent` (or chart helpers) | Concatenating free-form label lines from values | | ||
|
|
||
| Example — safe nested object (map → YAML, then indent): |
There was a problem hiding this comment.
| Example — safe nested object (map → YAML, then indent): | |
| ### Examples | |
| #### Safe nested object (map to YAML, then indent) |
| {{- toYaml .Values.config | nindent 4 }} | ||
| ``` | ||
|
|
||
| Example — unsafe patterns: |
There was a problem hiding this comment.
| Example — unsafe patterns: | |
| #### Unsafe patterns |
| When in doubt: | ||
|
|
||
| 1. Treat every `{{ ... }}` that lands inside YAML structure as untrusted input. | ||
| 2. Use `quote`, `toYaml`, and `nindent`/`indent` together rather than string | ||
| concatenation. | ||
| 3. Run `helm template` (and schema validation) in CI so malformed values fail | ||
| before they reach the cluster. | ||
|
|
There was a problem hiding this comment.
| When in doubt: | |
| 1. Treat every `{{ ... }}` that lands inside YAML structure as untrusted input. | |
| 2. Use `quote`, `toYaml`, and `nindent`/`indent` together rather than string | |
| concatenation. | |
| 3. Run `helm template` (and schema validation) in CI so malformed values fail | |
| before they reach the cluster. |
There was a problem hiding this comment.
Recommend moving this up next to the table, which is also describing best practices
Signed-off-by: Dean Chen <862469039@qq.com>
|
took the headings/shuffle — best practices sit with the table now, examples underneath. |
|
@TerryHowe the nindent/table bits from your review should be in — paige did a later edit pass as well. ptal when you have a minute? |
| | Goal | Prefer | Avoid | | ||
| | ---- | ------ | ----- | | ||
| | Quote a string scalar | `{{ .Values.name \| quote }}` | `{{ .Values.name }}` in a bare field | | ||
| | Multi-line free-form string | Store structured data as a map/list and use `toYaml`, or treat the whole value as one quoted scalar with `quote` | `{{ .Values.config \| nindent 4 }}` — `nindent` only indents; newline-bearing values can still inject keys | |
There was a problem hiding this comment.
Not sure what you mean by "multi-line free-form string"? This term is not used in the latest YAML spec.
YAML does have different mechanisms to express string scalars. These are:
- flow style:
- double-quoted
- single-quoted
- plain
- block style
- literal
- folded
I'm assuming that you are referring to one of the block styles, because these are the only ones that are typically written across multiple lines. (Yes, the flow styles can also be written across multiple lines, but the rules fore this are rather complex. I haven't seen this approach much, and I wouldn't recommend it.)
In either case, these are all string scalars. In contrast, you're mentioning map/list types in the "Prefer" column. These are collections (i.e. mappings or sequences) in YAML terminology, which are distinct from strings. I think the current phrasing of this whole row may confuse readers.
Btw, the YAML parser distinguishes between block scalars and block collections by block scalar headers. This involves either a | or a > character. If one of these headers is present, the YAML parser always treats the following block as a scalar, otherwise it treats it as a collection.
| # BAD: nindent alone still injects — it only adds spaces | ||
| data: | ||
| app.conf: | | ||
| {{ .Values.appConf | nindent 4 }} |
There was a problem hiding this comment.
Not sure about this. Have you tested it?
Since nindent adds spaces to each line of the value, this is actually sufficient to prevent injection.
However there is some nuance to this:
- The previous line contains a block scalar header, namely
|in this example.
If this were absent, this whole construct would be vulnerable to YAML injection. - The
nindentis called with the correct number, namely4in this example.
If this were2or smaller, this whole construct might be vulnerable to YAML injection.
However, I'm not 100% certain about this, due to the extra line-break in this example. We have 2 leading line-breaks here, one from the template itself, one from nindent. This may or may not work reliably.
In either case, I would recommend either of these alternatives instead:
- Use go template's
{{-which removes all preceding white-space, including the line break.
The leading white-space before{{-is is optional, but may improve readability of the template code:app.conf: | {{- .Values.appConf | nindent 4 }} - Use
indentrather thannindent, which does not add an extra line-break.
Here, it is essential that there is no additional indent before the{{.
Imho this makes the template code less readable:app.conf: | {{ .Values.appConf | indent 4 }} - Turn it into a one-liner.
Remove the line-break from the template, but retain the line-break thatnindentintroduces.
Could be confusing to some users, because a YAML block scalar header is not followed by content on the same line:app.conf: | {{ .Values.appConf | nindent 4 }}
| | ---- | ------ | ----- | | ||
| | Quote a string scalar | `{{ .Values.name \| quote }}` | `{{ .Values.name }}` in a bare field | | ||
| | Multi-line free-form string | Store structured data as a map/list and use `toYaml`, or treat the whole value as one quoted scalar with `quote` | `{{ .Values.config \| nindent 4 }}` — `nindent` only indents; newline-bearing values can still inject keys | | ||
| | Embed maps / lists | `{{ toYaml .Values.extraEnv \| nindent 8 }}`. `nindent` only adds indentation; it doesn't encode YAML. Pair it with `toYaml` for maps/lists, or use `quote` for a single scalar. Bare `nindent` on an untrusted string is still injectable. | `{{ .Values.extraEnv \| nindent 8 }}` without `toYaml`, or hand-rolled `key: {{ . }}` loops | |
There was a problem hiding this comment.
I'm missing some context here. Why is it nindent 8 rather than nindent 6 or nindent 23?
Of course, this depends on the indent of the surrounding YAML code in the template. If too little indent is used, this could still result in YAML injection. It should be documented how much indent is secure!
Imho this is at least 1 more than the indent of the surrounding YAML block. Though 2 more than the indent of the surrounding YAML block is commonly used, which improves readability of the generated YAML code.
Imho, this line also mixes statements about YAML collections and YAML scalars in a confusing manner. See my comment on the previous table row.
| | Quote a string scalar | `{{ .Values.name \| quote }}` | `{{ .Values.name }}` in a bare field | | ||
| | Multi-line free-form string | Store structured data as a map/list and use `toYaml`, or treat the whole value as one quoted scalar with `quote` | `{{ .Values.config \| nindent 4 }}` — `nindent` only indents; newline-bearing values can still inject keys | | ||
| | Embed maps / lists | `{{ toYaml .Values.extraEnv \| nindent 8 }}`. `nindent` only adds indentation; it doesn't encode YAML. Pair it with `toYaml` for maps/lists, or use `quote` for a single scalar. Bare `nindent` on an untrusted string is still injectable. | `{{ .Values.extraEnv \| nindent 8 }}` without `toYaml`, or hand-rolled `key: {{ . }}` loops | | ||
| | Labels / annotations maps | `toYaml` + `nindent` (or chart helpers) | Concatenating free-form label lines from values | |
There was a problem hiding this comment.
Not sure, if these deserve special mention?
From a YAML perspective, these are equivalent to other uses of collection types. Maybe add labels and annotations to the below code examples instead?
| apiVersion: v1 | ||
| kind: ConfigMap | ||
| metadata: | ||
| name: {{ include "mychart.fullname" . }} |
There was a problem hiding this comment.
Does the the include reference a standard template here?
If not, the template code here should not make any assumptions about what that included template generates. Instead, it should follow the above "best practices"!
In this example, the following might be most appropriate:
name: {{ include "mychart.fullname" . | quote }}
Btw, the existing docs for for the include function already get this right. Though the context is different in the examples there, so these use nindent instead of quote.
|
Hi all! Sorry, for meddling again. I really appreciate that you are taking YAML injection seriously. Frankly, I've discussed this topic elsewhere in the past, and some people didn't even acknowledge that there is a problem here. However, are you aware that there are two other PRs for the leading issue already? See #2158 and #2165. The latter one is from me. And I understand that the changes that it proposes may be too far reaching. I guess that I'm not involved in Helm enough to propose such bold changes. But could you at least cross-check the contents of this PR with the other two? Frankly, I fear that some of the new docs in this PR are incorrect. Or, at least, misleading and incomplete. See my comments above. May I also suggest to have test cases for all the new advice and examples? I guess that tests may not fit into this helm-www project nicely. But YAML syntax is really tricky and imho template code examples should be covered by tests! I have this helm-charts-yaml-injection project (I've mentioned it earlier) that tests all the advice that I've given on this topic in the past. If you think that it's missing a relevant test case, feel free to reach out to me. I'll happily add anything that might be relevant! |
There was a problem hiding this comment.
Pull request overview
This PR strengthens the Helm Chart Template Guide by adding a dedicated warning section about YAML injection risks when embedding .Values into rendered Kubernetes manifests, addressing the gap described in issue #2154.
Changes:
- Adds an early pointer to a new YAML injection prevention section in the YAML techniques appendix.
- Introduces guidance, best-practice recommendations, and examples showing safe vs unsafe patterns for embedding values into YAML.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| data: | ||
| # values.config is a map; toYaml emits nested YAML, nindent only indents it | ||
| config.yaml: | | ||
| {{- toYaml .Values.config | nindent 4 }} |
| | Quote a string scalar | `{{ .Values.name \| quote }}` | `{{ .Values.name }}` in a bare field | | ||
| | Multi-line free-form string | Store structured data as a map/list and use `toYaml`, or treat the whole value as one quoted scalar with `quote` | `{{ .Values.config \| nindent 4 }}` — `nindent` only indents; newline-bearing values can still inject keys | | ||
| | Embed maps / lists | `{{ toYaml .Values.extraEnv \| nindent 8 }}`. `nindent` only adds indentation; it doesn't encode YAML. Pair it with `toYaml` for maps/lists, or use `quote` for a single scalar. Bare `nindent` on an untrusted string is still injectable. | `{{ .Values.extraEnv \| nindent 8 }}` without `toYaml`, or hand-rolled `key: {{ . }}` loops | |
| # BAD: nindent alone still injects — it only adds spaces | ||
| data: | ||
| app.conf: | | ||
| {{ .Values.appConf | nindent 4 }} |
Signed-off-by: Dean Chen <862469039@qq.com>
|
switched the table to scalar vs collection, quoted the include, and showed |
The template guide shows
quote/toYaml/nindentin places, but there wasn't a single warning about what happens if you skip them. Added a short section under YAML techniques (with a pointer from the intro) on safe embedding of values.Fixes #2154