From 86e6eca7de65877a1e5341d1688d8151914a03c0 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 24 Sep 2026 16:31:37 +0200 Subject: [PATCH 1/2] test: define the several-document YAML loaders as deserialization sinks yaml.load_all, full_load_all and unsafe_load_all reached by HTTP input are insecure deserializations; load_all with a safe loader and safe_load_all are not. --- tests/test_safe_arguments.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_safe_arguments.py b/tests/test_safe_arguments.py index 329faa7..b46d6d9 100644 --- a/tests/test_safe_arguments.py +++ b/tests/test_safe_arguments.py @@ -112,3 +112,19 @@ def test_yaml_load_with_a_safe_loader_is_not_an_insecure_deserialization(loader: @pytest.mark.parametrize("loader", [None, "yaml.Loader", "Loader=yaml.FullLoader", "Loader=yaml.UnsafeLoader"]) def test_yaml_load_with_any_other_loader_still_is(loader: str | None) -> None: assert len(deserialization(loader)) == 1 + + +def several_documents(call: str) -> list[Finding]: + source = f"import yaml\nfrom flask import request\n\ndef read():\n return list({call})\n" + findings = engine.check(SourceManager().add_source("app.py", source), [PLUGINS]) + return [f for f in findings if f.rule_id == "insecure-deserialization"] + + +@pytest.mark.parametrize("function", ["load_all", "full_load_all", "unsafe_load_all"]) +def test_the_loaders_of_several_documents_are_deserialization_sinks_too(function: str) -> None: + assert len(several_documents(f"yaml.{function}(request.data)")) == 1 + + +@pytest.mark.parametrize("call", ["yaml.load_all(request.data, Loader=yaml.SafeLoader)", "yaml.safe_load_all(request.data)"]) +def test_the_safe_ways_to_load_several_documents_are_not(call: str) -> None: + assert several_documents(call) == [] From d16a9ea8c2bb7a0363a6779bfc9411bde865b0c9 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 24 Sep 2026 16:31:38 +0200 Subject: [PATCH 2/2] feat: make the several-document YAML loaders deserialization sinks yaml.load_all, yaml.full_load_all and yaml.unsafe_load_all load a stream of documents as their single-document versions do, but attacker input reaching them went unreported. They are sinks now; yaml.load_all shares the safe loaders of yaml.load through one constant. The usage guide lists the loaders the rule covers and the safe-loader exception. Closes #123 --- CHANGELOG.md | 4 +++ docs/usage.md | 2 +- .../models/python_stdlib/python_stdlib.py | 33 +++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9874ccd..9f9f334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Detection + +- `yaml.load_all`, `yaml.full_load_all` and `yaml.unsafe_load_all` are deserialization sinks like their single-document versions; `yaml.load_all` with a safe loader is not, and `yaml.safe_load_all` stays safe (#123). + ### Precision - A sink call can be made safe by one of its arguments: a `SafeArgument` model names the argument and the values that take kinds off the sink, only when the call gives one explicitly. `yaml.load` with `SafeLoader`, `BaseLoader` or their C versions, under every spelling, is no longer an insecure deserialization; an absent loader, another loader, a variable or unpacked arguments still are (#121). diff --git a/docs/usage.md b/docs/usage.md index 4566b63..67dda5d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -109,7 +109,7 @@ a sanitizer for that kind of sink. | `path-traversal` | A file system path (`open`, `send_file`, `os.remove`, …). | | `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`). | | `xss` | An HTTP response body built without escaping. | -| `insecure-deserialization` | `pickle`, `yaml.load` and similar loaders. | +| `insecure-deserialization` | `pickle`, `marshal`, `dill`, `jsonpickle`, `shelve`, and the YAML loaders (`yaml.load`, `load_all`, `full_load`, `unsafe_load` and their several-document versions); `yaml.load` and `load_all` given a safe loader (`SafeLoader`, `BaseLoader`) are not. | | `open-redirect` | An HTTP redirect target. | | `plaintext-credential-storage` | A parameter named like a password stored in a database without hashing. Medium confidence, since a name is a hint. | | `exploitable-vulnerability` | An API affected by an advisory of a vulnerable requirement, see Dependencies. | diff --git a/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py b/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py index 0aefa4f..65ee999 100644 --- a/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py +++ b/src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py @@ -17,6 +17,17 @@ ) _ENVIRONMENT_KINDS = TaintKind.ALL & ~(TaintKind.COMMAND | TaintKind.PATH) +# SafeLoader and BaseLoader build plain data only, in Python and in C, under every spelling. +_SAFE_YAML_LOADERS = ( + "python.yaml.SafeLoader", + "python.yaml.loader.SafeLoader", + "python.yaml.BaseLoader", + "python.yaml.loader.BaseLoader", + "python.yaml.CSafeLoader", + "python.yaml.cyaml.CSafeLoader", + "python.yaml.CBaseLoader", + "python.yaml.cyaml.CBaseLoader", +) _PROCESS_OUTPUT_KINDS = TaintKind.ALL & ~TaintKind.PATH @@ -74,23 +85,11 @@ class PythonStdlibModels(ModelPlugin): Sink(_sym("dill.loads"), TaintKind.DESERIALIZATION), Sink(_sym("jsonpickle.decode"), TaintKind.DESERIALIZATION), Sink(_sym("yaml.load"), TaintKind.DESERIALIZATION), - # SafeLoader and BaseLoader build plain data only, in Python and in C. - SafeArgument( - _sym("yaml.load"), - "Loader", - ( - "python.yaml.SafeLoader", - "python.yaml.loader.SafeLoader", - "python.yaml.BaseLoader", - "python.yaml.loader.BaseLoader", - "python.yaml.CSafeLoader", - "python.yaml.cyaml.CSafeLoader", - "python.yaml.CBaseLoader", - "python.yaml.cyaml.CBaseLoader", - ), - position=1, - kinds=TaintKind.DESERIALIZATION, - ), + Sink(_sym("yaml.load_all"), TaintKind.DESERIALIZATION), + Sink(_sym("yaml.full_load_all"), TaintKind.DESERIALIZATION), + Sink(_sym("yaml.unsafe_load_all"), TaintKind.DESERIALIZATION), + SafeArgument(_sym("yaml.load"), "Loader", _SAFE_YAML_LOADERS, position=1, kinds=TaintKind.DESERIALIZATION), + SafeArgument(_sym("yaml.load_all"), "Loader", _SAFE_YAML_LOADERS, position=1, kinds=TaintKind.DESERIALIZATION), Sink(_sym("yaml.unsafe_load"), TaintKind.DESERIALIZATION), Sink(_sym("yaml.full_load"), TaintKind.DESERIALIZATION), Sanitizer(_sym("os.path.basename"), TaintKind.PATH),