Why a finding should be raised on proof - not a 200-response heuristic.
Every single-page app returns 200 for a route that does not exist. The server hands back the same HTML shell for /admin, /billing and /this-page-is-imaginary, and the client-side router decides what to render. A scanner that reasons "200, and it is not a login page, therefore the resource is exposed" will raise a false positive on essentially every modern application. That single heuristic is responsible for a large share of the noise that makes security teams stop trusting their tools.
The mistake is testing the wrong layer. The render layer - what the browser paints - looks identical whether or not an authorization check exists behind it. The truth lives one layer down, in the data the API actually returns.
Ask the same application three questions and watch where the signal really is. The page shell answers 200 to everything; the API is where the boundary either holds or breaks:
So the rule is simple to state and strict to keep: we do not raise a finding because a route responded. We raise it because the data proved the boundary failed.
A finding is only raised when an exploit or leak is confirmed by one of three evidence classes - each of which is a fact about the response, not an inference about the code:
| Oracle | How it proves the issue | Example |
|---|---|---|
| In-band | The response itself carries data it should not - another tenant's record, a file's contents, a stack trace with secrets. | /orders/1002 returns user B's order to user A. |
| Differential | Two requests that should behave identically behave differently in a way only the vulnerability explains. | A true vs false SQL condition flips the row count. |
| Arithmetic / timing | A benign computed marker comes back evaluated, proving code ran without running anything dangerous. | 7919 * 7907 returns 62615533 from a template engine. |
The arithmetic oracle is worth dwelling on because it is how you prove code execution without ever executing an exploit. If a template or expression context returns 62615533 for the input 7919*7907, the engine evaluated it - that is server-side evaluation, demonstrated with a multiplication, not a shell command, a JNDI lookup, or a gadget chain. Proof of capability, zero blast radius.
Most tools collapse two different claims into one number. "The vulnerable code is reachable" and "the vulnerability was exploited" are not the same finding, and pretending they are is how reports get inflated. We keep them as separate states. Reachability says an attacker can get to the sink; proven-exploit says we drove the sink and it gave up something it should not have. A finding is escalated to confirmed only on the second.
That separation is not bookkeeping - it is what lets a team work the queue top-down with confidence, and it is why our OWASP Benchmark runs reach 0 false positives across categories. You cannot hit zero by guessing well; you hit it by refusing to raise anything you did not prove.
It would be easy to let the AI threat-modeling layer mark threats "proven" on its own say-so. We do not. A threat is labelled proven only when a real DAST finding backs it - the model can describe and prioritize a risk, but it can never self-certify that the risk is real. The exploit evidence is the only thing allowed to flip that bit.
Evidence over heuristics is not a slogan; it is a constraint we accept knowing it costs us findings a louder scanner would happily report. The trade is deliberate: a shorter list every item of which survives an audit beats a long list a reviewer stops reading at the first false positive.