Configuration feels inert. It is a file with settings in it, sitting next to your code, and nobody thinks of parsing it as processing untrusted input.
Several config formats can execute code, and several config loaders will do so by default. Once the file comes from anywhere other than your own repository, parsing it is a security decision.
This post is which formats carry that risk, where the untrusted config actually comes from, and the small set of rules that close it. For whoever loads a config file in a language with a permissive default.
The formats that can execute
YAML, in several languages, supports tags that instantiate arbitrary types. Python's yaml.load without a loader argument has been the canonical example, and equivalents exist elsewhere. The safe form is explicit:
yaml.safe_load(stream) # types limited to plain data
# not: yaml.load(stream) # can construct arbitrary Python objects
new Yaml(new SafeConstructor()) // rather than new Yaml()
JSON with polymorphic type handling. JSON itself is inert. A library configured to read a type name from the document and instantiate it is not, and that setting exists in several mainstream libraries for legitimate reasons. If your deserialiser accepts a class name from the document, the document chooses what gets constructed.
Formats that are literally a program. A config file evaluated as Ruby, Python, Lua or JavaScript is code by construction. That is fine for a file you wrote and a very different proposition for one you accepted.
Template-evaluated config. Environment interpolation, !include directives, and templating passes over configuration each add an evaluation step, and each has been a route to reading files that were not intended.
Where untrusted config comes from
The risk only matters if the file comes from somewhere you do not control. It does, more often than people expect:
- CI configuration in a pull request, from a fork. The build system reads a file the contributor wrote.
- Tenant-supplied configuration in a multi-tenant product: a workflow definition, a mapping, an export template.
- Configuration fetched at runtime from a remote service, where the trust boundary is the network path.
- Files inside an uploaded archive, where an extraction step reads a manifest.
- Anything a customer can edit in your product that is later parsed server-side.
The mental shortcut that fails is "config lives in our repo, so it is ours". Draw the line at where the file originates rather than what it is called.
The rules
Always use the safe loader, even for your own files. There is no cost, the file you control today may be generated tomorrow, and the default in several libraries is the unsafe one. Make it a lint rule so a future contributor cannot reintroduce it.
Never enable polymorphic deserialisation on anything that crosses a trust boundary. If you need it internally, scope it to an explicit allowlist of permitted types rather than accepting a class name from the document.
Validate against a schema before use. Parsing gives you a structure; a schema gives you the structure you expected. It also turns a malformed file into a clear error rather than a confusing failure three layers down.
Bound the parse. A config file can be a resource attack even when it cannot execute: deeply nested structures, enormous string expansion, references that multiply on expansion. Cap the input size, the nesting depth and the parse time for anything you did not write.
Do not resolve includes or remote references from untrusted config. An include directive that reads a path means the file chooses which file you read, which is a path traversal and a secret disclosure in one feature.
Config is also where the secrets leak
A separate failure with the same source. Config files accumulate credentials, and they end up somewhere they should not:
- Committed with a real value in place of a placeholder.
- Rendered into a container image as a build layer, where it persists even if a later layer removes it.
- Printed in full at startup by a helpful logging line that dumps effective configuration.
- Included in a support bundle or a diagnostic export.
That last one is worth a specific check. If your product has a "download diagnostics" feature, read the bundle it produces. They frequently contain the full effective configuration, credentials included, and they are sent over email to whoever asked.
Check yours
# Unsafe YAML loading
grep -rn "yaml.load(" --include=*.py . | grep -v safe_load
grep -rn "new Yaml()" --include=*.java .
# Polymorphic deserialisation enabled
grep -rn "enableDefaultTyping\|activateDefaultTyping\|TypeNameHandling" .
# Config dumped at startup
grep -rniE "log.*(config|settings|environment).*(dump|print|all)" .
# Then: does your diagnostics export contain secrets?
# Generate one and read it.
The concession
Most applications only ever parse configuration they wrote, shipped in their own image, and for them this is theoretical. Adding schema validation and bounded parsing to a file that has never come from outside is effort spent on a risk that does not exist.
So the trigger is the trust boundary, not the format. Audit the places where a config file arrives from a contributor, a customer or the network, and apply the rules there. Everywhere else, use the safe loader because it is free, and move on.
The implication
The category error is treating configuration as data because it looks like data. A parser is an interpreter, some config parsers are very capable interpreters, and the file gets to choose what they do.
The useful question is not what format you use. It is which of your config files were written by someone other than you, and what your parser is permitted to do when it reads one.