skip to content

An authenticated ZAP scan finishes anonymous with no error reported — how do you diagnose it?

level: seniorimportance: must knowfreq 55%

answer

  1. re-login is a retry, never a schedule
  2. only a negative verdict recovers a session
  3. three counters mean proceeding as authenticated
  4. no indicator configured returns authenticated
  5. the failure notice is desktop-only

basics

~20 s

Re-login is triggered only when the verification method returns a negative verdict, so a silent drop means verification kept saying yes. Read the five stats.auth.state. counters: four of them return authenticated and only one of those rests on a real match.

solid answer

~50 s

The tool does not re-authenticate on a schedule. The HTTP sender sends a request with the session stamped on, then asks the context's verification method about the result; only a **negative** verdict discards the cached session, drives a fresh login and resends that one request. So a run that drifts to anonymous is a run where verification kept returning *authenticated*. Of the five state counters, four record that outcome and only `loggedin` rests on a pattern having matched: `assumedin` means the poll cadence had not elapsed so nothing was checked, `unknown` means a logged-out pattern was configured and simply did not appear, and `noindicator` means no pattern was configured at all — in which case the check returns *authenticated* unconditionally and a re-login can never fire. In a headless run the human-readable failure notice is suppressed, so those counters and the log are the only trace.

go deeper

for a junior

Learn the one rule that explains the symptom: the tool only logs in again when its own check says the session is dead, and with no indicator configured that check always says it is alive.

for a middle

Walk the loop out loud — send, verify, and on a negative verdict discard, re-login and resend — and name which counter each possible verdict increments.

for a senior

Diagnose from evidence: counters first, then verification traffic in history, then the crawl surface comparison, and explain why a headless run gives you no human-readable warning at all.

for a principal

Decide what an authenticated scan has to prove before its result counts, and make that proof a machine-readable gate rather than a reviewer's impression of the findings list.

## The loop you are actually debugging Re-authentication is a **retry after the fact**, not a schedule. For one request, the live HTTP sender — which is the `network` add-on's sender; core keeps a deprecated one of the same shape — does this: 1. Stamps the user's session onto the request via the context's session-management method and sends it. 2. Asks the context's **verification method** whether that exchange looks authenticated. 3. If and only if the answer is **no**: discards the user's cached session, performs a fresh login, re-stamps, and **sends the same request again**. Two things fall out immediately. The first attempt of a failed pair really was sent, so it is in history and its response was recorded. And step 3 is the *only* thing that ever recovers a dead session — there is no timer, no keep-alive, no periodic re-login. If step 2 says *yes*, the run continues exactly as it is, for as long as it takes. The check is also skipped in three cases by design: for the login traffic itself, for the verification poll, and for requests whose header looks like an image request. ## The three ways step 2 says yes without being right Five counters cover every possible verdict. Four of the five return *authenticated*; the table's last column is what separates them. | counter | what happened | is it a real check? | |---|---|---| | `stats.auth.state.loggedin` | an indicator pattern matched | **yes** | | `stats.auth.state.assumedin` | the poll cadence had not elapsed, so the previous verdict was reused | no | | `stats.auth.state.noindicator` | no indicator pattern was configured at all | no | | `stats.auth.state.unknown` | a logged-out pattern was configured and failed to match | no — it is *assumed* in | | `stats.auth.state.loggedout` | the verdict was negative; a re-login will be attempted | yes | The `noindicator` row is the one that produces a whole run of anonymous traffic. With neither pattern configured, the check returns *authenticated* before it looks at anything, every time. A re-login can then never be triggered by verification, so the session that was established at the start is the only one the run will ever have; when the target expires it, the scan quietly continues as an anonymous user. The `assumedin` row produces the same effect in a narrower window, and `unknown` produces it whenever the only configured pattern is a logged-out one that the page happens not to contain. ## Why nothing complains - The *authentication failed* message is written to the desktop output panel, guarded on the interface being initialised. Headless, it is not written at all. The counters are incremented unconditionally, so they are the surviving signal. - The login's own success is judged by the **same** verification call. With no indicator configured, that call returns *authenticated*, so a failed login is recorded as a success. Worse, on the form and JSON login paths the session extracted from the login response is installed on the user **before** that verdict is taken, so the user now has a session — an unusable one — and no longer looks like it needs authenticating. - Nothing about the finding set changes shape when the session dies. The crawl simply stops reaching the authenticated surface. ## The diagnostic order 1. **Read the authentication-state counters for the site.** A run dominated by `noindicator` was never really checking. A run with a large `assumedin` and a non-zero `loggedout` was checking and losing the session repeatedly. 2. **Check `stats.auth.success` against `stats.auth.failure`.** Both are recorded unconditionally. Many successes means many re-logins, which is a session dying repeatedly rather than once. 3. **Pull the verification traffic from history.** Poll requests are tagged, so you can read exactly what the target replied when the tool decided it was still logged in. 4. **Compare the authenticated and anonymous crawl surfaces.** If the authenticated run found the same URL set as an anonymous one, the session was never carried at all — which is a session-management problem, not a verification one. ## The fix, in order of strength - **Configure a logged-in indicator.** This removes the unconditional *authenticated* return and is the single highest-value change. - **Choose the cadence unit deliberately**, so the unchecked window is bounded by scan volume rather than by wall-clock time. - **Assert on the counters in the pipeline** rather than on the report, so the run fails when the scan was not logged in — a defect the findings list cannot express.

  • Why does a failed form or JSON login still leave the user looking authenticated?
    On those paths the session extracted from the login response is installed on the user before the verdict is taken, and the test for whether a login is needed is simply whether that session object is null. A failed login therefore clears the *needs authentication* condition even though what it stored is unusable.
  • What stops two threads from re-authenticating the same user at once, and thrashing?
    Two guards. The login path is synchronised on the user, and the call that discards a session first compares the failing message's send time against the last successful authentication time. A stale failure that arrived after somebody else already re-logged in therefore does not throw away the fresh session.
  • Which requests never trigger a re-authentication attempt even if they look unauthenticated?
    The login traffic and the verification poll are excluded by their sender initiator, since otherwise the check would recurse. Requests whose header is recognised as an image request are also skipped, on the grounds that an image response is a poor place to look for a logged-in indicator.
  • Is a run with a high assumed-in count necessarily wrong?
    No — it is the expected result of polling rather than checking every request, and a zero would mean paying for a poll per request. It becomes a finding when it is paired with a non-zero logged-out count, because that says the session really was dying inside windows where nothing was being checked.

saying these in an interview costs you the question

  • Assumes a failed login aborts the scan
  • Expects the tool to re-authenticate on a timer
  • Reads a clean run as proof the scan stayed logged in
  • Believes an unconfigured indicator makes every response get checked
  • Thinks the session drop appears in the findings list