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.
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.
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.
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.
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.
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
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.
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" })// GOOD: Use httpOnly cookies instead of localStorage
response.cookies.set("session", token, {
httpOnly: true, // JS can't read it
secure: true, // HTTPS only
sameSite: "lax", // CSRF protection
maxAge: 3600 // 1 hour expiration
})
// GOOD: Short-lived JWTs with refresh tokens
const token = jwt.sign(payload, SECRET, { expiresIn: "15m" })
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: "7d" })Limits and what it needs
| Check | Reads | Reports when | Severity · confidence |
|---|---|---|---|
| Token in localStorage | all captured JavaScript | a localStorage.setItem or localStorage[…] with key token, access_token, auth_token or jwt; one finding per app | High · 0.80 |
| Cookie flags | cookies in the snapshot's response headers | a cookie whose name contains one of 10 auth names lacks HttpOnly (High) or Secure (Medium); one finding per missing flag | High · 0.85 / Medium · 0.85 |
| Token lifetime | the session JWT's exp claim | more than 24 hours remain; no exp or already expired is silent | Medium · 0.60 |
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.