PyYAML's yaml.load() does more than read data. On the default loader it can instantiate arbitrary Python objects encoded in the YAML, so parsing a hostile document can execute code. If the YAML comes from a request body, an uploaded file, or any other input you do not fully control, that is remote code execution. The fix is to call yaml.safe_load(), which only builds plain data types.
Why it's a problem
A YAML document can carry tags like !!python/object/apply that tell the full loader to call a Python callable while parsing. An attacker who controls the input can use that to run commands on your server. Because the vulnerable call looks like ordinary parsing and works fine on well-formed input, the danger stays invisible until someone sends a crafted document.
The pattern
import yaml
def load_config(raw: str):
# full loader: can construct arbitrary Python objects from the input
return yaml.load(raw)The fix
import yaml
def load_config(raw: str):
# safe_load only produces dicts, lists, strings, numbers, booleans
return yaml.safe_load(raw)Why AI tools write this
Older tutorials and a lot of training data use yaml.load() as the obvious way to parse YAML, and many examples predate the safe default. When an assistant wires up config loading or an import feature, it reaches for the call it has seen most often. It rarely distinguishes trusted config on disk from user-supplied input, so the unsafe call ends up on paths that read attacker-controlled data.
The quick fix
- Replace yaml.load(x) with yaml.safe_load(x) everywhere the input is not fully trusted.
- If you genuinely need custom tags, use yaml.load(x, Loader=yaml.SafeLoader) and add only the constructors you need.
- Never pass yaml.FullLoader or yaml.UnsafeLoader on request-derived input.
- Add a test that feeds a !!python/object payload and asserts it is rejected.