Detection Model
Every Dalfox finding answers three separate questions. Reading them as one scale is the most common source of confusion about the output, so each one has its own field:
| Axis | Field | Question |
|---|---|---|
| Confidence | type: V / R |
Can Dalfox claim this is a vulnerability? |
| Method | detection_method |
How was it found? |
| Impact | severity |
How bad is it if exploited? |
Only the first two vary independently today. For XSS findings, severity is a
restatement of the tier (V → High, A → Medium, R → Info), so
filtering or sorting on it tells you nothing that type doesn'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 is 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. Treat it as a signal and confirm it yourself. |
A |
AST-detected | Transitional: a method label. See Migration. |
I |
Informational | Not an XSS claim (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 does not mean the payload ran in a browser. 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. That is static analysis, on stronger evidence than the raw string match behind R.
Dalfox observes real execution in only one method: out-of-band callbacks (blind XSS with --blind-oob). When an injected <script src=…> calls home, something loaded the payload and fetched it. That is almost always a victim's browser, but any correlated DNS or HTTP interaction counts, so a server-side fetcher that follows the URL triggers it too. This is empirical evidence and the strongest Dalfox produces, but it comes from a client Dalfox does not control. A plain -b callback URL sends the hit to your own listener, so Dalfox records no finding for it.
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 | Not to find the flow; a flow found on a probe response gets one re-check request |
oob |
An out-of-band callback (DNS or HTTP) from whatever loaded the payload | Yes |
library |
<script> tags, for known-vulnerable versions |
No |
To select AST findings, use detection_method == "ast", not type == "A". The method field is stable; the tier is not.
The combinations that 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, which are 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) | 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 did so on a response it
already had instead of spending another request.
dom-verification evidence
A payload can prove it reached an executable position in five ways:
- the Dalfox marker matched by CSS selector
- an executable scheme (
javascript:,data:text/html,vbscript:) in a dangerous attribute - an injected element whose handler (or
<script>body) calls a sink - a sink call inside
<script>whose AST range covers the payload - an inline-handler breakout, where the payload ended the surrounding JS string
The evidence field names the one that fired.
What the response content type allows
The evidence has to hold in the parser a browser would use for that response, so the response's Content-Type decides which checks can succeed:
| Response type | How it is read |
|---|---|
text/html, or no usable Content-Type with a body that starts like HTML |
Parsed as HTML |
application/xhtml+xml, image/svg+xml, other XML (text/xml, application/xml, *+xml) |
Parsed as XML; only markup in an active namespace (XHTML, SVG) counts |
JavaScript (application/javascript, text/javascript, …) |
Read as script: only a JS-context payload such as a JSONP callback grades V, never HTML markup in the body |
application/json, *+json, text/csv, supplied text/plain, binary media |
Not a markup document: the reflection is dropped, with no R either |
text/plain is inert whether or not X-Content-Type-Options: nosniff is present, since a browser never sniffs a declared text/plain into HTML. The AST pass over the target page follows the same rule: it runs only when that page is an active markup document. One exception to the last row: a payload echoed in a redirect's Location header still records an R, whatever the body type.
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 the pass over the landing page: -p scopes which parameters get requested, and that pass reads a response Dalfox already has.
This also explains a result that looks like a gap but isn't. In a pure client-side DOM-XSS, JavaScript writes the payload into the page 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. To confirm, open the POC URL in a browser with devtools. Dalfox prints a complete POC URL on every AST finding. On landing-page findings it adds a [manual POC: …] setup hint for sources it can't 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 controls harvesting parameter names from the id/name attributes of <input> elements. 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 link.
location.*,document.URL, andURLSearchParamscarry the payload in the URL directly. Sources that would otherwise need an attacker-controlled driver page (window.name,document.referrer,postMessage, storage,history.state) gradelow, unless the page seeds that source from a query parameter itself, which a link can also drive. That case gradeshighwith the reasonnon-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 interception.
require-trusted-types-for 'script'with a TrustedHTML-class sink gradeslow.
Sanitizers are not a grading signal because they already act as a filter: the analyzer treats them as taint clearers, so if a finding exists at all, 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 only: flow sits inside a conditional branch records that the flow is guarded. It appears on high grades and never changes the grade on its own.
Where the grade is visible, and what reads it
json, jsonl, toml, markdown, and sarif carry confidence (with
confidence_reason). plain prints a Confidence: line with the grade
only. Three things act on it:
--min-confidence highdrops everylowfinding (everyR, plus AST flows Dalfox cannot stand behind) before anything else sees it: output in every format,--stream-findings, per-target counts intarget_summary,--baseline,--limit(the early stop and the display cut), and the exit code.Ifindings carry no grade (they are not XSS claims) and are always kept. The default,low, keeps everything. When it is set,meta.min_confidencereports{level, dropped}, so a target that readscleanbecause all of its findings were filtered can be told apart from one that found nothing. The same option ismin_confidencein the config file, the REST API, and MCP.- AST deduplication breaks a type + severity tie on the grade (
highfirst). - The exit code follows the filter above: a run whose only findings were
lowexits0under--min-confidence high.
--only-poc and --limit-result-type still select by type, and SARIF
level still follows severity (the grade is under properties).
Migration
The tier itself does not move yet. To act on the grade today, opt in with --min-confidence. You can see where each finding will land before the tier follows.
- Now:
typeis unchanged, anddetection_methodandconfidenceare available.type == "A"is deprecated as a selector; usedetection_method == "ast". - Next:
--tier-model confidenceas an opt-in. - Then: that becomes the default, with
--tier-model legacyas an escape hatch.Aretires:high-graded AST findings becomeVand the rest becomeR, which is whatRwas always for.Rgets a new name in that release too, because by then it will hold more than reflections.
--only-poc a keeps working throughout; once the tier is gone it selects detection_method == "ast". 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 assert; the tier reports what it has historically emitted. The disagreement is the preview signal, not a bug.
Once the tier derives 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 intended.
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>
└── Confidence: high
found reflected 0 params: the reflection method found no server-side reflection. Expected on a static site.XSS found 0 XSS: the headline count isVonly.(+3 A)names the other tiers printed below it.- The
[A]blocks come from the ast method, independent of both lines above.
If you read only the summary lines on a DOM-XSS target, you 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
Rcollapse. AnRis dropped when aVexists for the same(param, location, inject_type)on that target. Reporting both would list the same input twice at two different strengths.VandAare never dropped. - AST deduplication. The preflight, the probe, and the reflection loop can
all find the same source→sink flow. One survives per fingerprint:
the strongest by
type, thenseverity, thenconfidence. Type comes first, so alow-confidenceVstill outranks ahighA, unless--min-confidence highremoved theVbefore deduplication ran.
--stream-findings prints each scan-loop finding the moment it is recorded,
which is before the R collapse and before AST duplicates are narrowed to
the strongest. So an R can appear in the stream and be
absent from the final report. Findings from the landing-page AST pass,
outdated libraries, and OOB callbacks appear only in the final report. When the
two disagree, trust the final report.
Selecting tiers: --only-poc vs --limit-result-type
Both select by tier, but they do different things. --only-poc takes a comma list (v,r,a,i); --limit-result-type takes one of all (the default), v, r, a, i.
| 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 use up the budget |
--limit 2 --limit-result-type v means "keep scanning until two V findings
accrue, then show everything found along the way". To hide the rest,
add --only-poc v. With --only-poc, --limit counts only the tiers it
shows: a finding the report hides never stops the scan.
Exit codes
0 means no findings and 1 means at least one finding of any tier,
counted after --min-confidence, --only-poc, a --baseline filter, and the collapse above. 2
is a hard error (bad input, every target unreachable, --output unwritable).
It also covers a run with no findings that could not finish cleanly: a lost
session under the default --on-session-loss abort, a crashed scan worker, or
severe transport loss. A lone R, or a single I from --detect-outdated-libs, exits 1 the same way a V does.
For CI that should fail only on what Dalfox asserts is exploitable, run
--only-poc v; to fail only on claims graded high, run --min-confidence high.
The full rule is in Exit codes.
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) |
| Drop findings Dalfox cannot stand behind | --min-confidence high (I is kept) |