Skip to content

feat(detection): close the Python insecure-deserialisation gaps (#87) - #103

Merged
bkd-dotcom merged 1 commit into
mainfrom
feat/py-deserialization-gaps
Aug 18, 2026
Merged

feat(detection): close the Python insecure-deserialisation gaps (#87)#103
bkd-dotcom merged 1 commit into
mainfrom
feat/py-deserialization-gaps

Conversation

@bkd-dotcom

Copy link
Copy Markdown
Member

Closes #87. The gaps were identified by @AdvaitVarhade in #91 — credited in CONTRIBUTORS.md and the changelog.

Extends the existing block, rather than adding a second one

That distinction is the entire reason #91 couldn't merge as written. Findings dedup on (file, line, category), so a parallel rule using a different category name for the same class defeats the dedup and doubles every finding. One rule_id per class, one category.

Measured on the same 8-line fixture from that review:

findings
main before 2
#91 as written 4 (every one duplicated)
this PR 2

Now flagged

case before after
marshal.loads/load miss caught
shelve.open miss caught
yaml.unsafe_load miss caught
yaml.load(x, Loader=yaml.Loader) miss caught

That last one is the best catch of the set. The old condition was:

if target in ("yaml.load",) and not any(kw.arg == "Loader" for kw in node.keywords):

i.e. any Loader= counted as safe — so an explicitly unsafe loader passed silently. The Loader is now resolved and matched, not merely counted.

Also fixes a pre-existing false positive it exposed

Because the old check only inspected keywords, a positional safe loader was flagged:

yaml.load(x, yaml.SafeLoader)   # was a finding; it is safe

The Loader now comes from the keyword or the second positional argument, matched on its last path segment — so yaml.SafeLoader, a bare imported SafeLoader, and CSafeLoader are all recognised.

All five safe forms verified silent: Loader=yaml.SafeLoader · Loader=SafeLoader · positional yaml.SafeLoader · Loader=yaml.CSafeLoader · yaml.safe_load.

Verification

283 tests pass, ruff clean. Three new tests: the four newly-caught cases, the five safe forms, and a scan_repository test asserting exactly two findings on the duplicate fixture with a single category — which is the regression that would catch a future parallel-rule mistake.

On #91

I'll close it pointing here. @AdvaitVarhade found three real gaps that main genuinely missed — that's the hard part — and the review offered to land the patch with credit if they preferred not to rewrite. Credit is in CONTRIBUTORS.md and the changelog.

Closes #87. Gaps identified by @AdvaitVarhade in #91, credited in
CONTRIBUTORS.

Extends the EXISTING unsafe-deserialisation block rather than adding a
second one. That distinction is the whole reason #91 could not be merged
as written: findings dedup on (file, line, category), so a parallel rule
using a different category name for the same class defeats the dedup and
doubles every pickle/yaml finding in every scanned repo. One rule_id per
class, one category.

Now flagged:
  * marshal.loads / marshal.load  — executes arbitrary code while decoding
  * shelve.open                    — pickle-backed, same exposure
  * yaml.unsafe_load
  * yaml.load(x, Loader=yaml.Loader) — the best catch of the three. The
    old check was `not any(kw.arg == "Loader")`, i.e. ANY Loader counted
    as safe, so an explicitly unsafe loader passed silently.

Also fixes a pre-existing FALSE POSITIVE it exposed: the old check only
inspected keywords, so `yaml.load(x, yaml.SafeLoader)` — a positional safe
loader — was flagged. The Loader is now read from the keyword or the second
positional arg and matched on its last path segment, so `yaml.SafeLoader`,
a bare imported `SafeLoader`, and `CSafeLoader` are all recognised.

Verified: every case reports exactly ONCE (the 8-line fixture that went
2 findings -> 4 under #91 stays at 2), all five safe forms stay silent,
and 283 tests pass with ruff clean.
@github-actions

Copy link
Copy Markdown

Signetry Reviewer — 🟡 Needs human review

A human should decide — the required check is pending.

Deterministic gates (the authority)

Gate Status
Required status check ⏳ pending
Secret scan ✅ clean
CI permission / OIDC ✅ no forbidden change
Dependency skew ✅ ok
All green

Findings

No issues found by the deterministic scanners.

Merge

A human should review and merge.

This review is advisory. It never merges on its own judgement — the deterministic gates + a human are the authority. Findings can have false negatives; a green bot verdict is not a guarantee.

@bkd-dotcom
bkd-dotcom merged commit 358375b into main Aug 18, 2026
8 checks passed
@bkd-dotcom
bkd-dotcom deleted the feat/py-deserialization-gaps branch August 18, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detection: add insecure-deserialization rule for Python (pickle / yaml.load)

1 participant