Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
33 changes: 16 additions & 17 deletions src/coretrace_python/bundled/models/python_stdlib/python_stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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),
Expand Down
16 changes: 16 additions & 0 deletions tests/test_safe_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) == []
Loading