DAST scanner · reads the snapshot

Session Scanner

A stolen session is as good as a password. This scanner never sends a request: it reads the page snapshot that discovery already captured and looks for the three ways a session token gets stolen or stays valid too long: a token written to localStorage, an auth cookie without HttpOnly or Secure, and a JWT that expires more than a day out.

DASTHighAuth Weakness3 checksquick depth

Reading the snapshot

Phase 1 rendered the target with Playwright and kept the HTML, every JavaScript file it loaded and the response headers. Two of the three checks run entirely over that material. The actor here is the scanner alone; the target is never contacted again.

Snapshot from phase 1 captured once, before any scanner index.html app.3f2c.js vendor.9a1.js chunk-7.js Set-Cookie: sid=… Set-Cookie: theme=dark Set-Cookie: sb-access-token=… No request leaves this scanner. It only reads what is already here. 1 · Token storage regex over all JS keys: token · access_token · auth_token · jwt localStorage.setItem("token", res.jwt) in app.3f2c.js · first match wins, then stop chunk-7.js matches too: not reported again 2 · Auth cookie flags name must contain one of 10 auth names sidauth nameHttpOnly ✗ Secure ✗ themenot auth · skipped sb-access-tokenauth nameHttpOnly ✓ Secure ✓ each missing flag is its own finding Findings none yet HIGH · 0.80Auth token stored in localStorage HIGH · 0.85Cookie 'sid' missing HttpOnly MEDIUM · 0.85Cookie 'sid' missing Secure theme · sb-access-token: nothing JS: one finding, however many hits

1Phase 1 rendered the page and kept the HTML, every JavaScript file it loaded, and the response headers with their cookies. The session scanner sends nothing; it reads this snapshot.

2Two regexes run over the concatenated JavaScript, looking for localStorage.setItem( or localStorage[ with a key of token, access_token, auth_token or jwt. The first match is reported as HIGH, confidence 0.80, and the search stops.

3Cookies are filtered by name first. Only a name containing one of ten auth names (token, session, sid, jwt, sb-access-token, sb-refresh-token, __session, connect.sid …) is examined. theme is skipped without a look at its flags.

4Each auth cookie is checked twice. No HttpOnly → HIGH, 0.85: document.cookie can read it, so any XSS steals the session. No Secure → MEDIUM, 0.85: it travels over plain HTTP. sid yields two findings; sb-access-token has both flags and yields none.

5The guards: a cookie that is not an auth cookie never counts, a cookie with both flags is silent, and the JavaScript check reports once per app no matter how many files write the token.

Token lifetime

The third check needs a real session token. It decodes the JWT's payload without verifying the signature, because only one claim matters here: how far away exp is.

Session token from the authenticated crawl eyJhbGciOi….eyJzdWIiOi….SflKxw… payload, base64-decoded, not verified: { "sub": "u_42", "iat": 1792886400, "exp": 1795478400 } exp − now = 2 592 000 s = 720 h = 30 days Remaining lifetime now exp · 30 days out 24 h limit MEDIUM · 0.60Session token has very long expiration: expires in 720 hours (30 days) Quiet cases: no "exp" claimleft to the JWT scanner, not duplicated here exp already passednothing: an expired token is not a long-lived one

1The token is the one the crawler logged in with. Its middle segment is base64-decoded and parsed; the signature is not checked, because only the claims matter for this test.

2remaining = exp − now, in seconds, then hours. Here the token has 30 days left.

3More than 24 hours remaining → MEDIUM, confidence 0.60, with the hours and days in the description. A stolen token keeps working for all of that time.

4Three cases stay quiet: no exp claim at all (the JWT scanner reports that, so it is not duplicated), a token that has already expired, and anything at 24 hours or under.

Needs a session it is not given

The lifetime check only runs when the scanner holds an auth session with an access token. In engine/factory.py the standard DAST list constructs SessionScanner() with no session, so in the normal pipeline only the localStorage and cookie checks fire. The JWT scanner reports a token with no exp at all, but nothing else in the pipeline measures a token that expires far in the future, so this check is currently dormant.

The fix, flag by flag

One Set-Cookie header, written correctly, clears every finding this scanner makes. Each attribute closes one theft path.

What the app sends Set-Cookie: session=eyJhbGciOi…; Path=/; (no HttpOnly) HttpOnly; (no Secure, no SameSite) Secure; SameSite=Lax; (no Max-Age: the JWT inside lasts 30 days) Max-Age=3600 + expiresIn: "15m", refresh 7d and in the bundle: localStorage.setItem("token", res.jwt) // removed: the cookie carries the session Open findings HIGH token in localStorage HIGH 'session' missing HttpOnly MEDIUM 'session' missing Secure MEDIUM very long expiration

1The app as found: a session cookie with only a path, a JWT inside it that lasts 30 days, and a bundle that copies the token into localStorage for its API calls.

2httpOnly: true, and the localStorage copy is dropped. JavaScript, including an injected script, can no longer read the session at all. Two findings clear at once.

3secure: true keeps the cookie off plain HTTP, so it cannot be read on a shared network. sameSite: "lax" is not something this scanner checks, but it comes free in the same call and blocks CSRF.

4maxAge: 3600 on the cookie, a 15-minute access token and a 7-day refresh token. A stolen token is now worth minutes rather than a month.

Why it matters

Why it matters

localStorage + XSS = game over. Any script running on the page can read localStorage, so even a minor reflected XSS hands over the auth token. A cookie without HttpOnly is the same exposure through document.cookie; without Secure it is sent in the clear and readable on public Wi-Fi. And a long-lived token turns a momentary theft into days or weeks of access.

British Airways (2018)

Injected JavaScript on the payment pages could access client-side storage, enabling exfiltration of payment data and session tokens. Token theft via XSS remains one of the most common attack chains in modern web apps; a token kept out of JavaScript's reach is what stops the chain.

Vulnerable vs fixed code

// BAD: token readable by any script on the page
localStorage.setItem("token", data.token)

// BAD: cookie without flags: JS can read it, HTTP can carry it
response.cookies.set("session", token)

// BAD: a stolen token works for a month
const token = jwt.sign(payload, SECRET, { expiresIn: "30d" })

Limits and what it needs

CheckReadsReports whenSeverity · confidence
Token in localStorageall captured JavaScripta localStorage.setItem or localStorage[…] with key token, access_token, auth_token or jwt; one finding per appHigh · 0.80
Cookie flagscookies in the snapshot's response headersa cookie whose name contains one of 10 auth names lacks HttpOnly (High) or Secure (Medium); one finding per missing flagHigh · 0.85 / Medium · 0.85
Token lifetimethe session JWT's exp claimmore than 24 hours remain; no exp or already expired is silentMedium · 0.60
What it deliberately stays quiet about

It sends no request of its own, so it only sees cookies and scripts present in the captured snapshot; a cookie set later by an XHR login is out of view. Cookie names outside the ten auth names are never examined, and the JavaScript regexes match only the four listed keys, so localStorage.setItem("userSession", …) is not reported. It runs at quick depth with a 600 s timeout and finishes in seconds.