Skip to content

docs: warn about YAML injection in chart templates - #2211

Open
locker95 wants to merge 4 commits into
helm:mainfrom
locker95:docs/yaml-injection-guidance
Open

docs: warn about YAML injection in chart templates#2211
locker95 wants to merge 4 commits into
helm:mainfrom
locker95:docs/yaml-injection-guidance

Conversation

@locker95

@locker95 locker95 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

The template guide shows quote / toYaml / nindent in 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

| 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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This {{ .Values.config \| nindent 4 }} is still injectable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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...

@locker95

locker95 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — nindent only indents. I updated the table so maps/lists go through toYaml first and called out that bare nindent on free-form values is still injectable.

Signed-off-by: Dean Chen <862469039@qq.com>
@locker95

Copy link
Copy Markdown
Contributor Author

you're right — bare nindent doesn't encode anything. dropped it as a safe preference and called that out with a bad example.

@locker95
locker95 force-pushed the docs/yaml-injection-guidance branch from 6a67b7b to cc617dc Compare August 11, 2026 08:58
@locker95

Copy link
Copy Markdown
Contributor Author

@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 paigecalvert left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@locker95 a few comments. main issue was table rendering seems off!

Comment thread docs/chart_template_guide/yaml_techniques.md Outdated
Because Helm and Kubernetes often read, modify, and then rewrite YAML files, the
anchors will be lost.

## Safe embedding of values (YAML injection)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
## Safe embedding of values (YAML injection)
## Prevent YAML injection when inserting Helm values

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +366 to +367
the rendered YAML (YAML injection). That can break installs—or worse, inject
extra keys into a Kubernetes manifest.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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.

Comment on lines +378 to +380
`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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
`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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
[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`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
[function list](function_list.mdx) (`toYaml`, `quote`, `nindent`).
[Template Function List](function_list.mdx) (`toYaml`, `quote`, `nindent`).

Comment on lines +12 to +13
When you embed chart values into manifests, also read
[Safe embedding of values (YAML injection)](#safe-embedding-of-values-yaml-injection)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

note: if we change the section heading below this needs to be updated accordingly

Signed-off-by: Dean Chen <862469039@qq.com>
@locker95

Copy link
Copy Markdown
Contributor Author

escaped the table pipes so it actually renders, moved the nindent note into that row, and took your heading/wording/link edits.

@paigecalvert paigecalvert left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Example — unsafe patterns:
#### Unsafe patterns

Comment on lines +405 to +412
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommend moving this up next to the table, which is also describing best practices

Signed-off-by: Dean Chen <862469039@qq.com>
@locker95

Copy link
Copy Markdown
Contributor Author

took the headings/shuffle — best practices sit with the table now, examples underneath.

@paigecalvert paigecalvert left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/lgtm

@locker95

Copy link
Copy Markdown
Contributor Author

@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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

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.

Comment on lines +411 to +414
# BAD: nindent alone still injects — it only adds spaces
data:
app.conf: |
{{ .Values.appConf | nindent 4 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 nindent is called with the correct number, namely 4 in this example.
    If this were 2 or 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 indent rather than nindent, 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 that nindent introduces.
    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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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" . }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@meeque

meeque commented Aug 18, 2026

Copy link
Copy Markdown

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!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 }}
Comment on lines +377 to +379
| 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>
@locker95

Copy link
Copy Markdown
Contributor Author

switched the table to scalar vs collection, quoted the include, and showed {{- nindent N }} under a block header with N bigger than the parent indent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document how to prevent YAML-injection in Chart Templates?

5 participants