Why yaml load Is Dangerous and What to Use Instead
How a YAML tag makes PyYAML run code during load, a harmless demonstration, what changed in PyYAML 6 and the safe options for loading untrusted files.

A YAML file looks like data. In Python, with the wrong function, it can be a program. The mechanism is a feature, not a bug: YAML has tags, and PyYAML's full loader implements tags that name Python callables and then calls them. If you load a file you did not write with that loader, the author of the file decides what runs on your machine. This post shows the mechanism with a harmless example, explains what PyYAML changed, and lists what to use.
The mechanism
A YAML tag names a type. !!str 5 is a string, !!int "5" an integer. PyYAML adds tags that name Python objects: !!python/object:module.Class, !!python/object/apply:module.function and a few more. The apply form calls a function with arguments given in the document. So a document can say "call this function with these arguments" and the loader will do it as part of parsing:
!!python/object/apply:builtins.print ["code ran during load"]Loading it three ways:
safe_load: ConstructorError - could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply:built...
code ran during load
UnsafeLoader returned: None
yaml.load() with no Loader: TypeError load() missing 1 required positional argument: 'Loader'safe_load refused the tag. yaml.load(payload, Loader=yaml.UnsafeLoader) printed the message while parsing, and returned None, the return value of print. The text "code ran during load" appeared before the program had a chance to look at the data. Replace builtins.print with any importable callable and the document does that instead: os.system with a command string runs a shell command, subprocess.Popen starts a process, builtins.open and shutil.rmtree touch files. That is arbitrary code execution, with the permissions of the loading process, triggered by the content of a file.
What PyYAML changed
For years the documentation's first example was yaml.load(stream), which defaulted to an unsafe loader, and a great deal of code and tutorials copied it. In PyYAML 5.1 the default was changed to a restricted loader with a warning, and in 6.0 the Loader argument became mandatory. The third line of the output above is the result: calling yaml.load(payload) now raises TypeError instead of loading. This breaks old code loudly, which is the intent, and it nudges people to choose.
The available loaders, from least to most capable:
SafeLoader(andyaml.safe_load): basic YAML types only.FullLoader: more types, restricted Python objects; it was found to have bypasses and is not recommended for untrusted input.UnsafeLoaderandLoader: everything, including arbitrary callables. For trusted input only.
The rule is simple. If the file did not come from code you control, use safe_load. If you are not sure, it did not.
Who is at risk
The risky situations are quiet ones. A service that loads a YAML file uploaded by a user. A tool that reads a configuration file from a cloned repository, where the repository is someone else's. A command-line utility that processes YAML from a pipe. A web application that accepts YAML in a request body. A CI job that loads a file from a pull request submitted by an outside contributor. Each of these turns "load this config" into "run whatever the author of the config wants".
The same class of problem exists in other languages and libraries: Ruby's YAML.load has had the same history, Java's SnakeYAML constructor can instantiate arbitrary classes unless restricted, and several other ecosystems have had advisories. The principle carries over: a YAML loader that maps tags to code is a deserializer, and deserializing untrusted data into live objects is a well-known vulnerability class. Check the documentation of the library you use for its safe mode.
What to use
**yaml.safe_load or yaml.load(..., Loader=yaml.SafeLoader)** for everything that is configuration or data. It supports the standard types and nothing else. If your code gets a ConstructorError for a tag you did not expect, that is the loader doing its job.
**A subclass of SafeLoader with specific constructors** when you need a custom tag. For example, an !include or an !env tag implemented by you, registered on your own loader class. You control what the tag does, and nothing else is enabled. The !include demonstration in this cluster worked this way, and safe_load on the same file refused the tag.
**ruamel.yaml with typ="safe"** (or round-trip mode, which does not construct arbitrary Python objects). It follows YAML 1.2 and keeps comments in round-trip mode.
**Do not use FullLoader for untrusted input,** whatever its name suggests.
Avoid dumping Python objects as YAML for exchange. yaml.dump of an arbitrary object produces tags that only the unsafe loader reads. Convert to plain dictionaries and lists first and use safe_dump, so the output can be read safely by anyone (writing YAML from Python without ugly output).
Safe is not the same as harmless
safe_load removes code execution. It does not remove resource exhaustion. A malicious or buggy document can be very large, deeply nested, or built from aliases that expand exponentially in memory (the "billion laughs" idea applied to YAML, in which aliases refer to aliases of aliases). Parsing such a file can use gigabytes of memory or run for a long time, and safe_load will cooperate. For untrusted input, add limits before parsing:
Cap the size of the input (reject files above a few hundred kilobytes for configuration).
Cap the nesting depth by checking after loading, or with a pre-parse scan for runaway structures.
Consider disabling aliases altogether for untrusted documents, which a custom loader can do by raising on alias nodes.
Run the parse with resource limits (a time limit and a memory limit) in a separate process.
The same advice applies to XML (XML external entity attacks and limiting XML size and depth for uploads): safe parsing has two parts, no code execution and bounded resources.
Auditing existing code
Search your code base for the dangerous calls: yaml.load( without Loader=yaml.SafeLoader or CSafeLoader, yaml.load_all(, Loader=yaml.Loader, Loader=yaml.UnsafeLoader, Loader=yaml.FullLoader and yaml.unsafe_load. Static analysis tools flag them (Bandit has a rule for yaml.load). Treat each as a finding: either the input is trusted and a comment says why, or it changes to safe_load. Add the check to CI so new uses are caught. Also check the dependencies you use: a library that loads a YAML file on your behalf, for example a plugin or a template engine, may use an unsafe loader.
Related reading
The cluster hub is reading and writing YAML in Python safely. The safe call and its options are in loading YAML in Python with PyYAML safe_load. For the general principle, see safe XML parsing in Python with defusedxml. The change is described in the PyYAML 5.1 release notes and the class of vulnerability in the OWASP deserialization cheat sheet. Inspect a document's tags in the YAML formatter.
When safe loading alone is not enough
The input is untrusted and large. Add size, depth and alias limits, and resource limits on the process.
You deserialize into application classes. Even with safe loading, passing loaded dictionaries to something that instantiates classes by name reintroduces the risk. Validate against a schema first.
Other parts of the pipeline execute the content. If a value from the YAML ends up in a shell command or a SQL statement, the YAML parser is not the weak link. Escape or parameterize at that boundary.
You do not control the library that loads the file. A third-party tool with an unsafe loader is outside your fix. Update it, replace it or isolate it.


