Detection Model

Every Dalfox finding answers three separate questions. Reading them as one scale is the single most common source of confusion about the output, so they are separate fields:

Axis Field Question
Confidence typeV / R Can Dalfox claim this is a vulnerability?
Method detection_method How was it found?
Impact severity How bad is it if exploited?

Two of those three vary independently today. severity does not: for XSS findings it is currently a restatement of the tier (VHigh, AMedium, RInfo), so filtering or sorting on it tells you nothing that type didn't. It carries real information only on I, where it comes from the library advisory. Treat it as a display convenience until it grades impact on its own.

[A] predates that split. It answers the method question while sitting in the type field, which is why nobody (including the code) could say where it belonged on the confidence scale. It is being absorbed; see Migration below.

This page exists because the split was not visible from the output alone. It was worked out in issue #1238 with @OSTARA711, whose write-up is the basis for the model here.

Confidence: what type means

Tag Name Means
V Vulnerable Dalfox asserts this input is exploitable. Act on it.
R Reflected The payload came back in the response, but its position was not confirmed exploitable. A signal, not a claim — confirm it yourself.
A AST-detected Transitional — a method label. See Migration.
I Informational Not an XSS claim at all (e.g. a known-vulnerable JS library, CWE-1104).

Filter with --only-poc (e.g. --only-poc v, --only-poc v,a, --only-poc i).

What V does not mean

V is not browser execution. Dalfox drives no browser and speaks no CDP; it never renders a page or watches an alert() fire. For the request-based methods, V means the payload was found in a DOM tree parsed from a real HTTP response — static analysis, on stronger evidence than the raw string match behind R.

There is exactly one method where Dalfox observes real execution: out-of-band callbacks (blind XSS). When an injected <script src=…> calls home, a real browser parsed and fetched it. That is empirical, and it is the strongest evidence Dalfox produces — but it comes from someone else's browser, not one Dalfox controls.

Method: what detection_method means

Value Reads Sends a payload?
reflection The response body, for the payload's bytes Yes
dom-verification The response parsed as HTML, for an executable position Yes
ast The JavaScript in the response, for a source→sink flow No
oob An out-of-band callback from a real browser Yes
library <script> tags, for known-vulnerable versions No

Use detection_method == "ast", not type == "A", to select AST findings. The method field is stable; the tier is not.

The combinations that actually occur

The tier and the method are chosen separately, so V does not imply dom-verification. Every finding Dalfox emits is one of these:

type detection_method confidence severity Produced by
V dom-verification high High The dedicated DOM-verification request
V reflection high High The reflection phase, when the reflection response already parses to an executable position
V oob high High An out-of-band callback
V ast high or low High The two legacy AST promotions — the disagreement Migration is about
A ast high or low Medium Every other source→sink flow
R reflection low Info A reflection with no confirmed executable position, including the --hpp duplicate-parameter echo (inject_type: inHTML-HPP)
I library (absent) Low / Medium / High --detect-outdated-libs

V + reflection is the row that surprises people. reflection names which request the evidence came from, not how weak the evidence is: that row parsed the same DOM the dom-verification row does — it just did it on a response it already had, instead of spending another request.

dom-verification evidence

Five ways a payload proves it reached an executable position: the Dalfox marker matched by CSS selector; an executable scheme (javascript:, data:text/html) in a dangerous attribute; an injected element carrying a sink-calling handler; a sink call inside <script> whose AST range covers the payload; and an inline-handler breakout where the payload terminated the surrounding JS string. The evidence field names which one fired.

ast and the DOM-XSS ceiling

The AST pass parses the JavaScript in the response and traces data from a dangerous source (location.hash, location.search, document.referrer, postMessage, …) into a dangerous sink (innerHTML, document.write, eval, …) with no sanitizer on the path. It reads each <script> block once and reports every flow it finds, so it can name inputs you never passed on the command line — including a URL fragment, which is never sent to the server. -p does not narrow it: -p scopes which parameters get requested, and this pass sends nothing.

It also explains a result that looks like a gap but isn't. For a pure client-side DOM-XSS, the payload is written into the page by JavaScript at runtime, so it never appears in the server's response and the response-parsing methods have nothing to find. On a static page whose only sink is location.hash → innerHTML, --only-poc v correctly returns nothing. Open the POC URL in a browser with devtools to confirm — Dalfox prints a complete POC URL on every AST finding, plus a [manual POC: …] setup hint for sources it cannot put in a URL (window.name, document.referrer, cookies, postMessage, …).

Flag Effect
--skip-ast-analysis Turn off source→sink analysis
--analyze-external-js Also fetch and analyze same-origin <script src> bundles

--skip-mining-dom does not affect this pass — it governs harvesting parameter names from HTML id/name attributes. See Parameters & Discovery.

confidence: the grade behind the claim

Every XSS finding carries a confidence of high or low, plus a confidence_reason naming the deciding signals. For request-based methods it follows the evidence directly. For AST findings it is graded from the flow's shape:

high requires all of:

  • A source an attacker can reach with a linklocation.*, document.URL, URLSearchParams carry the payload in the URL directly. Sources that would otherwise need an attacker-controlled driver page (window.name, document.referrer, postMessage, storage, history.state) grade low, unless the page seeds that source from a query parameter itself, which a link can also drive. That case grades high with the reason non-URL source seeded from a query parameter by the page.
  • A payload the page's CSP would let execute — either inline script is permitted, or the sink runs script directly (eval, Function, document.write, <script> text) and so does not depend on inline-handler permission. A report-only CSP enforces nothing and never lowers the grade.
  • No Trusted Types interceptionrequire-trusted-types-for 'script' with a TrustedHTML-class sink grades low.

Sanitizers are not a grading signal because they are already a filter: the analyzer treats them as taint clearers, so a finding existing at all means no recognised sanitizer was on the path.

confidence_reason never mixes the two directions. A high grade lists the supporting signals; a low grade lists only what blocked it, so the signals that did hold are not shown. One reason is informational either way: flow sits inside a conditional branch records that the flow is guarded, and never changes the grade on its own.

Where the grade is visible — and where it isn't

confidence is carried by json, jsonl, toml, markdown and sarif. The default plain output does not print it, so the triage advice below assumes a machine-readable format. Nothing else keys off it yet either: --only-poc, --limit-result-type, the deduplication ranking, and the exit code all still read type. The grade is a preview of the migration, not yet a control.

Migration

That the grade drives nothing yet is the point: you can see where each finding will land before anything moves.

  1. Nowtype unchanged. detection_method and confidence are new. type == "A" is deprecated as a selector; use detection_method == "ast".
  2. Next--tier-model confidence as an opt-in.
  3. Then — that becomes the default, with --tier-model legacy as an escape hatch. A retires: high graded AST findings become V, the rest R, which is what R was always for. R is renamed in that release too: it will hold more than reflections by then, so the word stops being accurate at exactly that moment.

--only-poc a keeps working throughout; it selects detection_method == "ast" once the tier is gone. No flag value is ever removed.

During the transition type and confidence can disagree — a finding can read type=V, confidence=low. Two legacy code paths promote AST findings to V on evidence that does not support the claim; the grade reports what Dalfox can actually assert, the tier reports what it has historically emitted. The disagreement is the preview signal, not a bug.

When the tier does derive from the grade, everything that reads the tier follows it: the same --only-poc v selects a different set, the exit code flips on a different set, and deduplication ranks the survivors differently. That is the intended effect, not a side effect to work around.

Reading a mixed scan

INF found reflected 0 params
WRN XSS found 0 XSS (+3 A)
[POC][A][GET][DOM-XSS] https://target.app/?q=%3Cimg+src%3Dx+onerror%3D…
  ├── Issue: DOM-based XSS via URLSearchParams.get(q) to innerHTML (needs runtime confirmation)
  └── Payload: q=<img src=x onerror=alert(1) class=dlx1944740c>
  • found reflected 0 params — the reflection method found no server-side reflection. Expected on a static site.
  • XSS found 0 XSS — the headline count is V only. (+3 A) names the other tiers printed below it.
  • The [A] blocks come from the ast method, independent of both lines above.

Reading only the summary lines on a DOM-XSS target would miss every finding in the report.

Why the tiers don't sum to what was found

Two post-processing passes run before anything is printed, so the tier counts are not the number of findings recorded during the scan:

  • Redundant R collapse — an R is dropped when a V exists for the same (param, inject_type) on that target. Reporting both would list the same input twice at two different strengths. V and A are never dropped.
  • AST deduplication — the same source→sink flow can be found by the preflight, the probe, and the reflection loop. One survives per fingerprint: the strongest by type, then severity. confidence does not participate, so an ungraded V still outranks a high A.

--stream-findings emits each finding the moment it is recorded, which is before the collapse. An R can therefore appear in the stream and be absent from the final report. When the two disagree, the final report is the answer.

Selecting tiers: --only-poc vs --limit-result-type

Both take v / r / a / i, and they do different things:

Flag Effect
--only-poc Filters the output. Findings of other tiers are discarded
--limit-result-type Counts toward --limit. Findings of other tiers are still reported; they just don't consume the budget

--limit 2 --limit-result-type v means "keep scanning until two V findings accrue, then show everything found along the way". To actually hide the rest, add --only-poc v.

Exit codes

0 means no findings and 1 means at least one finding of any tier, counted after --only-poc and the collapse above. 2 is a hard error (bad input, every target unreachable, --output unwritable). A lone R, or a single I from --detect-outdated-libs, exits 1 exactly like a V does. For CI that should fail only on what Dalfox asserts is exploitable, run --only-poc v.

Choosing flags by intent

Goal Flags
Only what Dalfox asserts is exploitable --only-poc v
Suppress static-analysis noise on a production target --skip-ast-analysis
Skip name harvesting, keep DOM-XSS detection --skip-mining-dom
Test one parameter, still see every DOM sink -p q (AST findings are not scoped by -p)
Triage a large AST batch -f json, sort on confidence, then read confidence_reason
Fail CI only on asserted vulnerabilities --only-poc v (otherwise any tier exits 1)
ESC