Skip to content

How a Flow is Judged

Hugo edited this page Sep 25, 2026 · 1 revision

How a Flow is Judged

Finding that attacker data reaches a sink is the easy half. The analyzer then asks what the code does to that value on the way, and reports one of three verdicts. The verdict and its evidence are in the finding's metadata.

Verdict Result
Refuted Nothing is reported
Hotspot Reported at medium confidence
Vulnerability Reported at the rule's severity

Refuted: the guard proves the value safe

A dominating guard that establishes safety refutes the flow:

  • isdigit()
  • membership in a constant collection
  • equality with a constant
  • a numeric value proven by int(), len() or bounded arithmetic
  • a validator such as re.fullmatch

A validator only proves what it declares

A Validator proves its argument safe only for the kinds it declares. A validator declared for REDIRECT does not refute a command injection through the same value: that flow becomes a hotspot, and is refuted only when the validator covers every kind reaching the sink.

A Validator may also name a function of the project by its project symbol (python.hc.accounts.views._allow_redirect), and the refutation recognises it in its own module too, where a call to it has no imported symbol. A project can therefore declare its own validation helpers — see Plugins and Models.

Hotspot: something looked at the value, without proving anything

Two things lower a flow to a hotspot:

  • A guard that examines the data without proving it safe. A test of the value itself (if cmd:, len(cmd) < 10), of the same attribute, or of a method called on it (form.is_valid()).
  • An authorization decorator such as login_required.

Vulnerability: nothing on the path addressed the value

A guard on another attribute is no guard

if request.method == 'POST':
    os.system(request.POST['cmd'])      # still a vulnerability

The guard reads another attribute of the object the data comes from, so it says nothing about cmd. This is deliberate: treating it as a guard would quietly downgrade real findings.

When the flow passes the object whole — to the sink, or to a function — a guard on any of its attributes does count.

How the value reaches the sink

Each taint flow records how its value is passed to the sink, and an advisory entry point may name the attacker_arguments through which attacker input actually exploits it:

  • by keyword;
  • by position, when the argument may be passed positionally — a variadic one taking every later position.

Attacker input in another argument leaves the call reachable rather than exploitable. An advisory for send_from_directory naming its path no longer counts download_name=; one for requests.get naming its url no longer counts the body.

An argument unpacked with * or ** may fill any of them. Like a condition the analyzer cannot decide, it does not rule the call out — so a wrapper forwarding *args, **kwargs still reaches the entry point.

An entry point naming no argument counts every argument.

Suppressing a finding you disagree with

data = yaml.load(text)  # coretrace: ignore[insecure-deserialization]

A bare # coretrace: ignore suppresses every rule on that line; with a list, only those. It also works in requirements files:

pyyaml==5.3.1  # coretrace: ignore[vulnerable-dependency]

Suppressed findings are kept apart, not erased: the text report counts them, the JSON report lists them under suppressed, the SARIF log marks them suppressed in source, and they never affect the exit status.