SaaSFort
false positives external scanning evidence soft-404 scanner design

Why a 200 Is Not an Exposure: Soft-404s and Content Signatures

A single-page application answers 200 on every path, including /wp-config.php. A scanner that stops at the status code reports a critical credential leak on a stack that has never run PHP. How to gate an exposure finding on evidence.

ST
SaaSFort Team
· 5 min read · 914 words

Take a React or Vue application deployed behind a catch-all rewrite — try_files $uri /index.html on nginx, or the equivalent default on Netlify, Vercel and most static hosts. Client-side routing needs it: the server cannot know which paths the router owns, so it answers every unmatched path with the application shell, status 200.

Now point a probe list at that host. /wp-config.php returns 200. So does /.env, /.git/config, /backup.sql and /.htpasswd. A scanner that treats “the path returned 200” as “the file is served” reports a critical configuration leak — database credentials exposed to the internet — on a stack that has never executed a line of PHP.

That single finding is expensive in a way that is easy to underestimate. It does not cost you one line item in a report. It costs the report.

What the reader does with a false critical

A security department reading a supplier’s external audit does not verify findings one by one. It samples. It picks the most alarming item, checks it, and extrapolates from what it finds.

If the most alarming item is /wp-config.php exposed — critical and the reviewer opens the URL and sees your marketing homepage, the extrapolation is not “one finding was wrong”. It is “this tool does not verify its own claims, so I cannot use any of its output as evidence.” Everything else in the document — the TLS analysis, the header review, the DMARC record check — is now unsourced assertion. The report is discarded, and the questionnaire goes back to being answered by hand.

The asymmetry is worth stating plainly: a missed finding costs you coverage, a fabricated finding costs you credibility. They are not symmetric errors, and a scanner that optimises for recall without gating on evidence is optimising the wrong one.

Two gates before a finding exists

The fix is not a heuristic about which frameworks tend to catch-all. It is to make the scanner prove the claim before it makes it.

Gate 1 — learn the host’s default answer. Before probing anything real, request paths that certainly do not exist: a random nonce path, and the same with a .php extension so a host that special-cases one extension is still characterised. If the host answers 200 to those, it has a catch-all, and its default response is recorded: status, content-type, body length, and a hash of the body with digits and long hex strings normalised away — asset fingerprints, nonces, request ids and timestamps differ between two renders of the same shell and would otherwise defeat an exact hash.

Any probe whose response is indistinguishable from that baseline is the default page, not the artefact requested. In practice a length-ratio fallback around 0.92 absorbs the per-route jitter of a shell that embeds a differing <title> or a preloaded route, without swallowing genuinely distinct files.

Gate 2 — check the body against a signature for the artefact claimed. A .git/config contains [core] or repositoryformatversion. A wp-config.php contains <?php, DB_NAME or AUTH_KEY. A .DS_Store starts with Bud1. A SQL dump contains CREATE TABLE or INSERT INTO. An .htpasswd contains a hash prefix — $apr1$, $2y$, {SHA}. A Spring actuator returns JSON with "status".

The corollary matters as much as the rule: several of these artefacts are never legitimately served as text/html. A git config rendered as HTML is a repository viewer, not the file. Rejecting the HTML content-type for those paths removes an entire class of false positive on its own.

A response has to clear both gates. Clearing the first without the second is the interesting case — a host with no catch-all that returns 200 and an HTML error page for /backup.sql. Status alone would flag it; the signature does not match, so nothing is reported.

Suppressed, not silently dropped

A gate that discards findings invisibly is its own credibility problem — the reader has no way to distinguish “checked and clean” from “checked, matched, and quietly filtered”. Every suppression should carry its reason: response identical to the host’s catch-all page observed at /saasfort-nonexistent-1f3k9x (200, text/html), or body does not carry a wp-config.php signature. The suppression stays auditable. Someone who disagrees with the gate can see exactly what the gate saw.

The same principle applies to the findings that survive. A confirmed exposure should ship with the matched signature and a body excerpt, so the reader verifies the claim rather than trusting the tool. That is the difference between a scanner that produces alerts and one that produces evidence: an alert asks to be believed, evidence asks to be checked.

What this does not fix

Content signatures address one failure mode: claiming a file is exposed when it is not. They do nothing for the opposite error, and nothing for depth.

An artefact served under a non-standard path is not probed, so it is not found. An artefact behind authentication is out of scope for an unauthenticated external scan by construction. And no amount of signature matching turns external probing into a penetration test: business-logic flaws, chained authorisation bugs and multi-step attack paths require a human with credentials and a scope document. External-surface analysis is complementary to a manual engagement, not a substitute for one.

What it does buy is the thing a report is for. When a reviewer picks the loudest finding in your document and checks it, the check passes.

Related reading: what auditors actually ask for under NIS2 Article 21, and what an external scan can and cannot prove.

Ready to put this into practice?

Two ways to start — pick what fits. Free Scan if you want to see your security grade in 60s with no commitment. Free 14-day Growth trial if you're ready to monitor multiple domains, export NIS2 reports, and download Deal Reports — no credit card required.

No credit card · Cancel anytime · GDPR-ready · EU-hosted

Continue reading