ML CVEs
A process diagram of connected decision steps
defense

How to Triage an ML-Stack CVE: A Practical Workflow

A repeatable workflow for taking an ML-library CVE from 'a scanner flagged it' to a defensible decision — without panic-patching everything or trusting the CVSS number to do your thinking.

By ML CVEs Editorial · · 8 min read

A scanner flags transformers, torch, vllm, or langchain with a fresh CVE and a scary number. The wrong move is to either patch everything reflexively or wave it off because “we’ll get it next cycle.” Both skip the only step that produces a defensible answer: figuring out whether your deployment can actually reach the vulnerable code with input an attacker controls. Here is the workflow we run on every ML-stack CVE, in order.

Step 0: Confirm the CVE is real and read the primary record

Before anything else, pull the NVD entry and read the description verbatim. This is not a formality. CVE IDs get rejected, disputed, or quietly re-scoped, and downstream feeds lag the source. We have seen IDs circulate in blog posts that NVD lists as rejected. A disputed entry — common in ML, where “trusting your peers” is sometimes by design rather than a bug — changes the conversation entirely.

Capture from the primary record:

  • Exact affected package and version range
  • The CWE class (it predicts the mitigation; CWE-502 deserialization behaves nothing like a ReDoS)
  • The CVSS vector, not just the number (attack vector, whether user interaction is required)
  • Whether the status is anything other than “Analyzed/Published”

If the description does not match what your scanner claimed, trust the primary record.

Step 1: Classify the bug into an exposure family

ML-stack CVEs cluster into a small number of families, and the family tells you most of what you need:

FamilyTypical CWEWhat it costs youWhat gates it
Deserialization / load-time RCECWE-502Code execution on the hostLoading an untrusted artifact
Unsafe dynamic executionCWE-94/95Code executionA config flag or eval path
ReDoS / resource exhaustionCWE-400/1333Worker hang, DoSAttacker-controlled text through a specific path
Memory safety (native ops)CWE-122/787Crash, sometimes worseA crafted tensor through a specific kernel
Path traversal / archiveCWE-22/345File write, scan bypassExtracting an untrusted archive

The first two are the ones that justify dropping everything. The rest are real but almost always gated on a specific code path you may not even use.

Step 2: Read the patch, not just the advisory

The NVD entry rarely names the vulnerable function precisely enough. The fix commit always does. Open the GitHub Security Advisory (GHSA) if one exists, follow it to the fix PR, and note:

  • The exact function or class that changed
  • The public API that reaches it (this is what you grep your code for)
  • The input shape that triggers it: a model file? a tokenizer string? a config JSON? an archive?

Fifteen minutes reading a diff routinely reverts a “critical, patch now” into “we never call that path,” and occasionally promotes a “medium” into “we call that on every request with user input — fix today.”

Step 3: Map it to your deployment

Now the only question that actually scores risk: does our pipeline reach the vulnerable path with bytes we did not produce and verify?

  • Grep the codebase for the entry-point APIs from Step 2.
  • For each hit, classify the input: trusted internal artifact, or untrusted (community checkpoint, user upload, runtime Hub pull, user free-text)?
  • Check whether any required configuration flag is set (for example trust_remote_code=True, an unsafe-load toggle, or weights_only=False).
  • Separate runtime inference from offline training/fine-tuning — a path that is safe in production may be exposed in your training pipeline, which is slower-moving but real.

The output is a single net-exposure rating: critical / high / medium / low / not applicable. “Not applicable” is a legitimate and common verdict, and reaching it deliberately is the whole point.

Step 4: Decide the action

Map exposure to action rather than mapping CVSS to action:

  • Critical/High and reachable with untrusted input: patch now (pin to the fixed version), and audit what already passed through the path before the patch.
  • Gated on a flag you set: disable the flag if you can; patch on the next cycle if you cannot.
  • Medium and reachable only in training: schedule for the training-pipeline maintenance window.
  • Not applicable: record why (which path, why unreachable) and patch on the normal cadence anyway, because “not applicable today” can become applicable when someone adds a feature.

Always record the reasoning, not just the verdict. The next person to see this CVE in a scanner should not have to redo the analysis.

Step 5: Add detection and a structural fix

Patching closes one instance; the family stays open. For each triaged CVE, ask whether a structural change retires the whole family:

  • For deserialization CVEs, moving weights to safetensors removes the sink, not just this entry.
  • For unsafe-execution flags, defaulting them off across the org prevents the next one.
  • For all of them, pinning artifacts by digest and scanning in CI catches the variant that has not been assigned a CVE yet.

Then add a log signature for attempted exploitation where feasible, so a future attempt is visible rather than silent.

A worked micro-example

A scanner flags transformers with a deserialization RCE.

  1. NVD: real, published, CWE-502, affects versions below a known fix line.
  2. Family: load-time RCE — the high-stakes bucket.
  3. Patch: the change is in a model/config loading path reached by from_pretrained against a repository.
  4. Deployment map: we from_pretrained from a public Hub repo at startup → untrusted input, path reachable → high, reachable.
  5. Action: pin to the fixed version now; audit which model revisions we have loaded.
  6. Structural fix: switch those models to safetensors and pull by digest so the next entry in this family is inert.

Total time: well under an hour, and the outcome is defensible to an auditor in a sentence.

Why the workflow beats the scanner

A vulnerability scanner produces a list sorted by a number computed against a generic threat model. This workflow produces a list sorted by your exposure, with a recorded reason for each verdict. The difference shows up the first time you can tell leadership “twelve flagged, three actually reachable, all three patched, here’s why the other nine aren’t urgent” — instead of either a nine-alarm fire drill or a shrug. Most teams run the scanner and call it done. The defensible work is everything after the scan.

See also

Sources

  1. NVD CVE Details Database
  2. FIRST CVSS v3.1 Specification
  3. CWE-502: Deserialization of Untrusted Data — MITRE
  4. OpenSSF Scorecard
  5. OWASP Top 10 for LLM Applications
Subscribe

ML CVEs — in your inbox

CVEs in ML libraries, frameworks, and the AI/ML supply chain. — delivered when there's something worth your inbox.

No spam. Unsubscribe anytime.

Related

Comments