Detect session fixation and low-entropy session ID vulnerabilities across web frameworks (Express/Node, Django, Spring Boot, PHP, Rails) using a three-phase approach: recon (map every authentication handler and session configuration site), batched parallel verification (confirm that session IDs are not regenerated on privilege escalation or are generated with a non-cryptographic PRNG), and merge (consolidate findings into sast/session-results.md and sast/session-results.json). Covers CWE-384 ...
Installs into .claude/skills of the current project.
Are you the author of Sast Session?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mstfknn-sast-session)
---
name: sast-session
description: >-
Detect session fixation and low-entropy session ID vulnerabilities across
web frameworks (Express/Node, Django, Spring Boot, PHP, Rails) using a
three-phase approach: recon (map every authentication handler and session
configuration site), batched parallel verification (confirm that session IDs
are not regenerated on privilege escalation or are generated with a
non-cryptographic PRNG), and merge (consolidate findings into
sast/session-results.md and sast/session-results.json). Covers CWE-384
(Session Fixation) and CWE-330 (Use of Insufficiently Random Values).
Stateless JWT flows are out of scope (use sast-jwt instead). Outputs
sast/session-results.md and sast/session-results.json when complete.
version: 0.1.0
---
# Session Fixation and Low-Entropy Session ID Detection
You are performing a focused security assessment to find session fixation vulnerabilities and low-entropy session token generation in web applications. This skill uses a three-phase approach with subagents: **recon** (map every authentication handler and session configuration site), **batched verify** (confirm each candidate is exploitable with no effective mitigation), and **merge** (consolidate into canonical output files).
This skill targets **OWASP Web Top 25 A07 — Identification and Authentication Failures**, specifically:
- **CWE-384 Session Fixation**: The session ID is not regenerated after a privilege escalation event (login, sudo, role-switch, MFA completion). An attacker who knows or controls a victim's pre-auth session ID can hijack the session immediately on login.
- **CWE-330 Use of Insufficiently Random Values**: The session token is generated using a non-cryptographic PRNG (`random.random()`, `Math.random()`, `rand()`, `time()`) rather than a CSPRNG, making it predictable and enumerable.
**Out of scope**: Stateless JWT flows (use `sast-jwt`); session IDs correctly regenerated on every privilege change; tokens generated by a well-audited framework default using a CSPRNG (e.g., `express-session` with its default `genid` backed by `uid-safe`).
---
## What is Session Fixation
HTTP sessions are typically implemented as an opaque token (the session ID) stored in a cookie, header, or query parameter. The server maps this token to a server-side session store that holds the user's identity, roles, and other state.
**The core attack pattern** for session fixation:
1. Attacker visits the application and obtains a valid pre-authentication session ID.
2. Attacker tricks the victim into using that session ID (e.g., via a crafted link with `?PHPSESSID=attacker-known-value` or by injecting a `Set-Cookie` header through an open redirect or subdomain takeover).
3. Victim authenticates — the application sets the user attributes on the **existing** session without issuing a new session ID.
4. Attacker's pre-existing knowledge of the session ID now gives them an authenticated session.
The fix is mandatory: **always regenerate the session ID before or immediately after setting any user-identifying attributes on session login success**. This severs the link between the pre-auth and post-auth session.
### What Session Fixation IS
**1. Authenticate-then-assign without regenerate (direct)**
The application writes user identity to the session immediately after verifying credentials, without first regenerating the session ID:
```javascript
// Express/Node — VULNERABLE
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
req.session.userId = user.id; // ← session ID unchanged; fixation possible
req.session.role = user.role;
res.json({ ok: true });
});
```
**2. Django `login()` without `cycle_key()`**
Django's `login()` helper authenticates the user and stores the user object in the session, but it does **not** always rotate the session key in older versions or when called without `cycle_key()` (Django <= 4.1 without `SESSION_COOKIE_SECURE`). The secure pattern is to call `request.session.cycle_key()` before `login()`:
```python
# Django — VULNERABLE
from django.contrib.auth import login
def login_view(request):
user = authenticate(request, username=request.POST['username'],
password=request.POST['password'])
if user:
login(request, user) # ← session key may not have rotated
return redirect('/dashboard')
```
**3. PHP `$_SESSION` assignment without `session_regenerate_id(true)`**
PHP session IDs are opaque strings. Without calling `session_regenerate_id(true)` (the `true` deletes the old session file), the pre-login ID survives:
```php
// PHP — VULNERABLE
session_start();
if (verify_login($_POST['user'], $_POST['pass'])) {
$_SESSION['user_id'] = get_user_id($_POST['user']); // ← no regeneration
header('Location: /dashboard');
}
```
**4. Rails `session[:user_id] =` without `reset_session`**
Rails' `reset_session` clears and rotates the session. Omitting it leaves the session ID stable across login:
```ruby
# Rails — VULNERABLE
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id # ← no reset_session before assignment
redirect_to dashboard_path
end
end
```
**5. Spring Boot without `sessionFixation().newSession()` or `.changeSessionId()`**
Spring Security's security filter chain should be configured to create a new session or change the session ID on authentication. Omitting this configuration or explicitly disabling it is vulnerable:
```java
// Spring Boot — VULNERABLE
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
// ← no sessionManagement() → sessionFixation default varies by Spring version
return http.build();
}
// Also vulnerable:
http.sessionManagement(session ->
session.sessionFixation().none() // ← explicitly disabled
);
```
**6. Low-entropy session ID generation (CWE-330)**
When an application generates its own session IDs rather than relying on the framework, it may use a non-cryptographic source:
```python
# Python — VULNERABLE: uses random.random() (Mersenne Twister, predictable)
import random, string
def generate_session_id():
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# Python — VULNERABLE: seeds with time (predictable)
import random, time
random.seed(int(time.time()))
session_id = str(random.getrandbits(128))
```
```javascript
// Node.js — VULNERABLE: Math.random() is not cryptographic
function generateSessionId() {
return Math.random().toString(36).substr(2) + Math.random().toString(36).substr(2);
}
```
```php
// PHP — VULNERABLE: rand() or mt_rand() are predictable
$session_id = md5(rand() . time() . $_SERVER['REMOTE_ADDR']);
```
### What Session Fixation is NOT
Do not flag these as session fixation vulnerabilities:
- **`req.session.regenerate()` called before assignment**: The session ID changes on login; fixation is not possible.
- **Django `cycle_key()` called before `login()`**: The session key rotates; this is the secure pattern.
- **`reset_session` in Rails sign-in**: Session is cleared and rotated; secure.
- **Spring `sessionFixation().newSession()` or `.changeSessionId()`** globally configured: Spring Security handles rotation; not vulnerable.
- **Framework-managed CSPRNG session IDs with no custom `genid`**: `express-session` with default `uid-safe`, Django's default session key generation, PHP's `session_regenerate_id(true)` — all use CSPRNG.
- **Stateless JWT flows**: No server-side session to fix; covered by `sast-jwt`.
- **Session expiry / timeout bugs**: These are session management quality issues but not fixation or entropy issues in the CWE-384/CWE-330 sense.
### Patterns That Prevent Session Fixation
**Express/Node — regenerate then assign**
```javascript
// SECURE: regenerate() invalidates old session ID and creates a new one
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.role = user.role;
res.json({ ok: true });
});
});
```
**Django — cycle_key before login**
```python
# SECURE: cycle_key() rotates the session key before associating the user
from django.contrib.auth import login
def login_view(request):
user = authenticate(request, username=request.POST['username'],
password=request.POST['password'])
if user:
request.session.cycle_key() # ← rotate first
login(request, user)
return redirect('/dashboard')
```
**PHP — session_regenerate_id with delete**
```php
// SECURE: true deletes the old session file, preventing reuse
session_start();
if (verify_login($_POST['user'], $_POST['pass'])) {
session_regenerate_id(true); // ← rotate with delete
$_SESSION['user_id'] = get_user_id($_POST['user']);
header('Location: /dashboard');
}
```
**Rails — reset_session before assignment**
```ruby
# SECURE: reset_session clears session data and rotates the session ID
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
reset_session # ← rotate first
session[:user_id] = user.id
redirect_to dashboard_path
end
end
```
**Spring Boot — sessionFixation configured**
```java
// SECURE: changeSessionId() or newSession() in the security filter chain
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixation().changeSessionId() // ← rotate on authentication
)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
```
**Python CSPRNG session ID generation**
```python
# SECURE: secrets module uses OS CSPRNG
import secrets
def generate_session_id() -> str:
return secrets.token_hex(32) # 256 bits of entropy
```
---
## Vulnerable vs. Secure Examples
### Express/Node
```javascript
// VULNERABLE: No regenerate() call before setting userId
app.post('/login', async (req, res) => {
const user = await db.users.findByCredentials(req.body.email, req.body.password);
if (!user) return res.status(401).end();
req.session.userId = user.id; // ← Session ID unchanged → fixation
req.session.isAdmin = user.isAdmin;
res.json({ success: true });
});
// SECURE: regenerate() in callback, then assign
app.post('/login', async (req, res) => {
const user = await db.users.findByCredentials(req.body.email, req.body.password);
if (!user) return res.status(401).end();
req.session.regenerate((err) => {
if (err) next(err);
req.session.userId = user.id;
req.session.isAdmin = user.isAdmin;
req.session.save((err) => { if (err) next(err); res.json({ success: true }); });
});
});
// ALSO SECURE: custom genid using crypto (CSPRNG)
const session = require('express-session');
const { randomUUID } = require('crypto');
app.use(session({
secret: process.env.SESSION_SECRET,
genid: () => randomUUID(), // ← CSPRNG; not fixation-related but note
resave: false,
saveUninitialized: false,
}));
// VULNERABLE: hardcoded short secret (entropy issue, not fixation)
app.use(session({ secret: 'abc123' }));
```
### Django
```python
# VULNERABLE: login() called without cycle_key()
from django.contrib.auth import authenticate, login
from django.shortcuts import redirect
def sign_in(request):
if request.method == 'POST':
user = authenticate(request,
username=request.POST['username'],
password=request.POST['password'])
if user is not None:
login(request, user) # ← session key not guaranteed to rotate
return redirect('dashboard')
# VULNERABLE: custom session key using random
import random
def make_session_key():
return ''.join([str(random.randint(0, 9)) for _ in range(40)])
# SECURE: cycle_key() before login()
def sign_in(request):
if request.method == 'POST':
user = authenticate(request,
username=request.POST['username'],
password=request.POST['password'])
if user is not None:
request.session.cycle_key()
login(request, user)
return redirect('dashboard')
# VULNERABLE settings
# settings.py
SESSION_COOKIE_SECURE = False # ← session cookie sent over HTTP; amplifies hijack
```
### Spring Boot
```java
// VULNERABLE: sessionFixation explicitly disabled
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session ->
session.sessionFixation().none() // ← explicitly disabled
)
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
// VULNERABLE: no sessionManagement at all in older Spring Security versions
// (Spring Security < 4.x defaulted to none())
// SECURE: changeSessionId() (default in Spring Security 5+, but always explicit)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixation().changeSessionId()
.maximumSessions(1)
)
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
// VULNERABLE: HttpSession attribute set after custom auth without Spring Security
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req,
HttpSession session) {
User user = authService.authenticate(req.getUsername(), req.getPassword());
if (user == null) return ResponseEntity.status(401).build();
session.setAttribute("userId", user.getId()); // ← no invalidate/new session
return ResponseEntity.ok().build();
}
// SECURE: invalidate old session and create new one manually
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req,
HttpServletRequest request) {
User user = authService.authenticate(req.getUsername(), req.getPassword());
if (user == null) return ResponseEntity.status(401).build();
HttpSession oldSession = request.getSession(false);
if (oldSession != null) oldSession.invalidate();
HttpSession newSession = request.getSession(true); // ← fresh session ID
newSession.setAttribute("userId", user.getId());
return ResponseEntity.ok().build();
}
```
### PHP
```php
// VULNERABLE: no session_regenerate_id() before writing user data
<?php
session_start();
if (check_credentials($_POST['username'], $_POST['password'])) {
$_SESSION['user_id'] = get_user_id($_POST['username']); // ← fixation
$_SESSION['role'] = get_user_role($_POST['username']);
header('Location: /dashboard');
exit;
}
// VULNERABLE: low-entropy ID with md5(rand())
$custom_id = md5(rand() . microtime());
session_id($custom_id);
session_start();
// SECURE: regenerate with deletion, then assign
<?php
session_start();
if (check_credentials($_POST['username'], $_POST['password'])) {
session_regenerate_id(true); // ← true = delete old
$_SESSION['user_id'] = get_user_id($_POST['username']);
$_SESSION['role'] = get_user_role($_POST['username']);
header('Location: /dashboard');
exit;
}
// SECURE: use random_bytes() for custom session ID if you must
$custom_id = bin2hex(random_bytes(32)); // ← CSPRNG
session_id($custom_id);
session_start();
```
### Rails
```ruby
# VULNERABLE: session[:user_id] set without reset_session
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:session][:email])
if user&.authenticate(params[:session][:password])
session[:user_id] = user.id # ← no reset_session
redirect_to root_path
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
end
# VULNERABLE: rand() used for token generation
def generate_token
rand(36**40).to_s(36) # ← Math.random-class; predictable
end
# SECURE: reset_session before assignment
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:session][:email])
if user&.authenticate(params[:session][:password])
reset_session # ← rotate session ID and clear state
session[:user_id] = user.id
redirect_to root_path
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
end
# SECURE: SecureRandom for token generation
def generate_token
SecureRandom.hex(32) # ← CSPRNG; 256 bits
end
```
---
## Execution
This skill runs in three phases. Pass the contents of `sast/architecture.md` to every subagent as context.
### Phase 1: Recon — Map Authentication Handlers and Session Configuration
Launch a subagent with the following instructions:
> **Goal**: Locate every authentication handler, session configuration site, and custom session ID generation function in the codebase. Write results to `sast/session-recon.md`. Do **not** assess exploitability in this phase — that is Phase 2's job.
>
> **Context**: You will be given the project's architecture summary. Use it to understand the tech stack, framework, and authentication layer before searching.
>
> **What to search for**:
>
> **1. Session middleware / store configuration**
>
> Express/Node:
> - `require('express-session')`, `import session from 'express-session'`
> - `app.use(session({` — note the `secret`, `genid`, `resave`, `saveUninitialized` options
> - Look for `secret:` values that are hardcoded short strings vs. loaded from `process.env`
> - Look for custom `genid:` functions — do they use `crypto.randomUUID()` / `crypto.randomBytes()` or `Math.random()`?
> - Other session libraries: `cookie-session`, `connect-mongo`, `@fastify/session`
>
> Django:
> - `SESSION_ENGINE`, `SESSION_COOKIE_SECURE`, `SESSION_COOKIE_HTTPONLY`, `SESSION_COOKIE_SAMESITE` in `settings.py`
> - `SESSION_COOKIE_AGE` — note value; very long lifetimes amplify stolen session impact
> - Custom session backends in `SESSION_ENGINE`
>
> Spring Boot:
> - `HttpSession`, `SessionManagementConfigurer`, `sessionFixation()` calls in `SecurityFilterChain` beans
> - `@EnableSpringHttpSession`, `@EnableRedisHttpSession` — custom session stores
> - Any `HttpSession.setAttribute` calls outside of Spring Security's managed flow
>
> PHP:
> - `session_start()`, `session_id()`, `session_regenerate_id()` calls
> - `session.cookie_secure`, `session.cookie_httponly`, `session.use_strict_mode` in `php.ini` or `ini_set()`
>
> Rails:
> - `session_store` in `config/initializers/session_store.rb` or `config/application.rb`
> - `:secret_key_base` and `:key` options
>
> **2. Authentication handlers — login / sign-in endpoints and methods**
>
> Express/Node:
> - Route handlers: `app.post('/login', ...)`, `router.post('/signin', ...)`, `router.post('/auth', ...)`
> - Passport.js: `passport.authenticate(...)`, `req.logIn(user, ...)` — check if `req.session.regenerate()` is called within the `req.logIn` callback
> - Look for the assignment pattern: `req.session.userId =`, `req.session.user =`, `req.session.account =`
> - Critical: is there a `req.session.regenerate(` call **before** the assignment?
>
> Django:
> - `from django.contrib.auth import login` — every call to `login(request, user)`
> - Check whether `request.session.cycle_key()` is called before/after `login()`
> - Class-based views: `LoginView` subclasses that override `form_valid()`
>
> Spring Boot:
> - `successfulAuthentication(...)` overrides in `AbstractAuthenticationProcessingFilter` subclasses
> - Custom `AuthenticationSuccessHandler` implementations
> - Direct `HttpSession.setAttribute(...)` calls within authentication flows
> - `sessionManagement().sessionFixation()` in `SecurityFilterChain` — note if `none()`, `newSession()`, `changeSessionId()`, or `migrateSession()` (deprecated)
>
> PHP:
> - Files containing both `session_start()` and `$_SESSION['user']` / `$_SESSION['user_id']` / `$_SESSION['username']` assignments
> - Look for `session_regenerate_id(` — is it called with `true` (delete old)? Is it called at all?
>
> Rails:
> - Controllers containing `session[:user_id] =` or `session[:current_user] =`
> - Check for `reset_session` call immediately before the assignment
> - Devise gem: `sign_in(user)` — note whether `Devise::SessionsController` is customized; Devise handles fixation by default since v3.1
>
> **3. Custom session ID or token generation**
>
> Across all languages, search for:
> - `Math.random()`, `math.random()` in session ID construction
> - `random.random()`, `random.randint(`, `random.choice(` used for session or token values
> - `rand()`, `mt_rand()` in PHP session ID construction
> - `rand(` in Ruby without `SecureRandom`
> - `time()`, `microtime()`, `Date.now()`, `time.time()` seeding or used directly in session IDs
> - Patterns: `md5(rand() . time())`, `sha1(microtime())`, `base64(rand()*timestamp)`, `str(random.getrandbits(...))`
>
> **4. Privilege escalation events beyond initial login**
> Also flag:
> - Role-switch endpoints (sudo mode, assume-role, switch-tenant)
> - MFA completion handlers — session should be regenerated after MFA passes
> - Password reset completion — session should be regenerated to prevent token-to-session confusion
>
> **Output format** — write to `sast/session-recon.md`:
>
> ```markdown
> # Session Security Recon: [Project Name]
>
> ## Summary
> - Framework: [Express / Django / Spring Boot / PHP / Rails / other]
> - Session library: [express-session v3.x / Django built-in / Spring Session / PHP built-in / Rails cookie store / etc.]
> - Session ID generation: [framework default CSPRNG / custom — describe]
> - Session fixation mitigation present: [yes / no / partial / unknown]
>
> ## Session Middleware / Store Configuration
>
> ### [Config site name — e.g., "express-session setup in app.js"]
> - **File**: `path/to/file.ext` (lines X-Y)
> - **Secret source**: [env var / hardcoded string — value if short]
> - **genid**: [default uid-safe / custom function — describe]
> - **Cookie flags**: [secure: true/false, httpOnly: true/false, sameSite: value]
> - **Code snippet**:
> ```
> [relevant session() / session_start() / etc. call]
> ```
>
> ## Authentication Handlers
>
> ### [Handler name — e.g., "POST /login in routes/auth.js"]
> - **File**: `path/to/file.ext` (lines X-Y)
> - **Function / route**: [name or path]
> - **Session assignment**: [`req.session.userId = user.id` / `login(request, user)` / `session[:user_id] = user.id` / etc.]
> - **Session regeneration**: [present — `req.session.regenerate()` at line N / absent / cycle_key() / reset_session / session_regenerate_id(true)]
> - **Regeneration order**: [before assignment / after assignment / not present]
> - **Code snippet**:
> ```
> [the authentication block including any regenerate call and session assignment]
> ```
>
> ## Custom Session ID / Token Generation
>
> ### [Generator name if found]
> - **File**: `path/to/file.ext` (lines X-Y)
> - **PRNG used**: [Math.random() / random.random() / rand() / secrets / crypto.randomBytes() / SecureRandom / etc.]
> - **Code snippet**:
> ```
> [the generation function]
> ```
>
> ## Privilege Escalation Handlers Beyond Login
>
> ### [Handler name — e.g., "MFA completion handler"]
> - **File**: `path/to/file.ext` (lines X-Y)
> - **Event type**: [MFA completion / role switch / sudo / password reset]
> - **Session regeneration**: [present / absent]
> ```
### After Phase 1: Check for Session Usage Before Proceeding
After Phase 1 completes, read `sast/session-recon.md`. If no session-based authentication was found (summary indicates JWT-only or no server-side sessions), write the following to `sast/session-results.md` and `sast/session-results.json`, then stop:
`sast/session-results.md`:
```markdown
# Session Security Results
No server-side session-based authentication detected in this codebase. If JWT is used, see sast/jwt-results.md.
```
`sast/session-results.json`:
```json
{ "findings": [] }
```
Only proceed to Phase 2 if at least one authentication handler with session assignment was found.
### Phase 2: Batched Verify — Confirm Exploitability
Group all candidate authentication handlers and session configuration sites from `sast/session-recon.md` into batches of **3 candidates each**. For each batch, launch a **parallel subagent**. Name the batch output files `sast/session-batch-1.md`, `sast/session-batch-2.md`, etc.
Each batch subagent receives the following instructions:
> **Goal**: For each candidate in your assigned batch, determine whether a session fixation or low-entropy session ID vulnerability is present and exploitable. Write your findings for this batch to `sast/session-batch-N.md` (replace N with your batch number).
>
> **Context**: You will be given:
> - The project's architecture summary (`sast/architecture.md`)
> - The recon output (`sast/session-recon.md`)
> - The specific candidates in your batch (list provided below)
>
> **For each candidate, work through the following checks in order**:
>
> **Check 1 — Session fixation: is session ID regenerated before or immediately after login?**
>
> Read the authentication handler file at the specified location. Trace the entire code path from credential verification to session data assignment:
>
> - Express/Node: Is `req.session.regenerate(callback)` called synchronously before any `req.session.<user-attr> =` assignment? Note: the assignment MUST be inside the callback, not before it. Regenerate outside the callback does nothing useful because the callback may fire asynchronously after the assignment already ran.
> ```javascript
> // BAD — assignment happens before regenerate callback fires
> req.session.userId = user.id; // ← already assigned
> req.session.regenerate(() => { ... }); // too late
>
> // GOOD — assignment is inside the callback
> req.session.regenerate((err) => {
> req.session.userId = user.id; // ← happens after rotation
> });
> ```
> - Django: Is `request.session.cycle_key()` called on the same request object before `login(request, user)`? Check for it in the same view function or in a middleware that runs before the login call.
> - PHP: Is `session_regenerate_id(true)` called before `$_SESSION['user*'] =` assignment? The `true` argument is required; `session_regenerate_id()` without `true` keeps the old session file and is insufficient against some fixation variants.
> - Rails: Is `reset_session` (or `request.reset_session`) called before `session[:user_id] =`? Note: Devise's `sign_in` handles this automatically since Devise 3.1 — if the app uses Devise without overriding the sessions controller, it is likely safe.
> - Spring Boot: Is `sessionManagement().sessionFixation().changeSessionId()` or `.newSession()` present in the `SecurityFilterChain`? Also check if `.none()` explicitly disables it. For custom non-Spring-Security flows, does the handler call `request.getSession(false).invalidate()` followed by `request.getSession(true)`?
>
> **FP killers — do NOT flag if any of the following are true**:
> - `req.session.regenerate()` is called and the user attributes are set ONLY inside its callback (Express)
> - `request.session.cycle_key()` is present before `login(request, user)` (Django)
> - `session_regenerate_id(true)` is called before `$_SESSION` assignment (PHP)
> - `reset_session` is called before `session[:]` assignment (Rails)
> - Spring's `sessionFixation().changeSessionId()` or `.newSession()` is configured globally (Spring Boot)
> - The application is using Devise's default `SessionsController` without customization
> - The "session" is actually a JWT stored in a cookie (stateless — not fixation-vulnerable)
> - The handler is a token refresh endpoint, not a login endpoint
>
> **Check 2 — Low-entropy session ID generation: is the PRNG cryptographic?**
>
> If a custom session ID generator was found in recon:
> - Identify the PRNG function used.
> - Is it a CSPRNG? (`crypto.randomBytes`, `crypto.randomUUID`, `secrets.token_hex`, `secrets.token_urlsafe`, `random_bytes()`, `SecureRandom.hex`)
> - Is it a non-CSPRNG? (`Math.random()`, `random.random()`, `random.randint`, `rand()`, `mt_rand()`, `time()`, `microtime()`)
> - For non-CSPRNG, assess whether the output is actually used as the session ID or only as one input among others. If the session ID is a hash of a non-CSPRNG value plus a constant, the entropy is still low.
>
> **FP killers for entropy**:
> - The function is not used for session IDs, only for CSRF tokens or nonces that are separately validated server-side
> - The PRNG output is combined with a CSPRNG-generated value (effectively making the total entropy high)
> - The framework overrides the custom generator with its own CSPRNG in production
>
> **Check 3 — Hardcoded or weak session secret (Express/Node)**
>
> If `express-session` (or similar) was configured with `secret:`:
> - Is the secret hardcoded as a string literal in the source file?
> - Is the hardcoded secret short (< 20 characters) or a common word?
> - Is there any fallback logic that uses a hardcoded default if the env var is not set? (e.g., `secret: process.env.SESSION_SECRET || 'changeme'`)
>
> A weak secret allows an attacker to forge `connect.sid` cookies by brute-forcing the HMAC signature, which is equivalent to session hijacking without fixation. Classify as medium if the secret is loaded from env (env misconfiguration is an operational risk, not a code vulnerability) and high/critical if hardcoded.
>
> **Check 4 — Django SESSION_COOKIE_SECURE = False**
>
> If `SESSION_COOKIE_SECURE = False` or the setting is absent (default is `False` in Django < 4.0):
> - This does not directly cause fixation, but it means the session cookie can be transmitted over HTTP, enabling network-level session hijacking that fixation or entropy issues would exploit.
> - Flag as medium if this is the only issue; do not flag as a standalone critical finding unless combined with a fixation vulnerability (chain_id: "session-hijack").
>
> **Severity and exploitability classification**:
>
> | Condition | Severity | Exploitability | Confidence |
> |---|---|---|---|
> | Session fixation: direct authenticate-then-assign without any regeneration | high | reachable | high |
> | Session fixation: regeneration present but assignment is OUTSIDE the callback (Express async mistake) | high | reachable | high |
> | Session fixation: `session_regenerate_id()` without `true` in PHP | medium | conditional | medium |
> | Session fixation: Spring `sessionFixation().none()` explicit | high | reachable | high |
> | Session fixation: no sessionManagement() in older Spring Security | high | conditional | medium |
> | Low-entropy PRNG: Math.random() / random.random() / rand() for session ID | high | reachable | high |
> | Low-entropy PRNG: time-seeded but combined with CSPRNG component | low | conditional | low |
> | Hardcoded session secret (short/common) | high | reachable | high |
> | Hardcoded session secret (long, env-var-looking but not) | medium | reachable | medium |
> | SESSION_COOKIE_SECURE = False (no fixation found) | medium | conditional | high |
>
> Raise severity to **critical** if:
> - The application handles financial transactions, medical records, or PII and session is the sole auth mechanism
> - The endpoint is an admin login or privilege-escalation handler
>
> Lower severity to **medium** if:
> - Session TTL is < 5 minutes (short-lived sessions reduce fixation window)
> - The application enforces secondary validation (e.g., IP binding, device fingerprinting) that would break a fixed session
>
> **chain_id assignment**:
> - If SESSION_COOKIE_SECURE = False or HttpOnly = False co-occurs with a fixation finding: set `chain_id: "session-hijack"` on both findings
> - If no rate limiting on login (if sast-ratelimit findings reference the same login endpoint): set `chain_id: "credential-stuffing"` on the low-entropy session ID finding
>
> **Output format** — write to `sast/session-batch-N.md`:
>
> ```markdown
> # Session Security Batch N Findings
>
> ## Candidate: [Handler or config name from recon]
> - **File**: `path/to/file.ext` (lines X-Y)
> - **Vulnerability class**: [Session Fixation — no regeneration / Session Fixation — regeneration after assignment / Low-Entropy Session ID / Hardcoded Session Secret / Insecure Cookie Flag]
> - **Verdict**: [VULNERABLE / NOT VULNERABLE / NEEDS MANUAL REVIEW]
> - **Reasoning**:
> [Explain what the code does step-by-step and why it is or is not vulnerable. Reference specific line numbers. Explicitly state which FP killers were checked and whether they apply.]
> - **FP killers checked**:
> - [ ] Framework-default regeneration (Devise, Spring Security default, etc.)? [yes/no — why]
> - [ ] Regeneration before assignment? [yes/no — where]
> - [ ] CSPRNG used for custom ID? [yes/no / N/A]
> - [ ] Session is stateless JWT? [yes/no]
> - **Severity**: [critical / high / medium / low / info]
> - **Exploitability**: [reachable / conditional / unreachable / unknown]
> - **Confidence**: [high / medium / low]
> - **chain_id**: [session-hijack / credential-stuffing / null]
> - **Attack scenario**:
> [Step-by-step: what the attacker does, what preconditions exist, what session ID they use, what access they gain.]
> - **Proof-of-concept**:
> ```
> [Concrete reproduction steps using curl, browser DevTools, Burp Suite, or a short script.
> Example for Express fixation:
> 1. GET /login → server sets session cookie: connect.sid=s%3Aattacker-value
> 2. POST /login with victim's credentials (attacker knows the cookie value)
> 3. GET /dashboard with cookie connect.sid=s%3Aattacker-value → now authenticated as victim]
> ```
> - **Remediation**: [Specific fix referencing the exact function and argument needed]
> ```
### Phase 3: Merge — Consolidate and Emit Canonical Output
After **all batch subagents from Phase 2 complete**, launch a final merge subagent:
> **Goal**: Read all `sast/session-batch-*.md` files. Deduplicate findings (same file + line + vulnerability class = one finding), rank by severity (critical then high then medium then low then info), and write the final human-readable report to `sast/session-results.md` and the machine-readable canonical JSON to `sast/session-results.json`. Then delete all intermediate files (`sast/session-recon.md`, `sast/session-batch-*.md`).
>
> **Deduplication rules**:
> - Two findings with the same file, approximate line range (within 5 lines), and vulnerability class are the same finding. Keep the one with higher confidence; merge the attack scenarios.
> - A fixation finding and a cookie flag finding on the same endpoint are separate findings but share `chain_id: "session-hijack"`.
>
> **sast/session-results.md format**:
>
> ```markdown
> # Session Security Results: [Project Name]
>
> ## Executive Summary
> - Authentication handlers analyzed: [N]
> - Findings: [N critical / N high / N medium / N low / N info]
> - Key risks: [one-line summary of the most impactful issues]
>
> ## Findings
>
> ### [CRITICAL|HIGH|MEDIUM|LOW|INFO] — [Vulnerability class]: [Short description]
> - **ID**: session-[N]
> - **File**: `path/to/file.ext` (line X)
> - **Severity**: [critical / high / medium / low / info]
> - **Exploitability**: [reachable / conditional / unreachable / unknown]
> - **Confidence**: [high / medium / low]
> - **chain_id**: [session-hijack / credential-stuffing / null]
> - **Description**: [Full description of what is wrong and why it is exploitable]
> - **Attack scenario**: [Concrete step-by-step attack]
> - **Proof-of-concept**:
> ```
> [PoC commands or steps]
> ```
> - **Remediation**: [Exact fix with code example]
>
> ## Chains
>
> ### session-hijack
> Findings that compose into a full session hijack attack chain:
> [List of finding IDs that share this chain_id with a one-line explanation of how they combine]
>
> ### credential-stuffing
> Findings that compose into a credential-stuffing amplification chain:
> [List of finding IDs]
>
> ## Not Flagged (Key True Negatives)
> [List any handlers that were analyzed and confirmed NOT vulnerable — this shows coverage]
> ```
>
> **sast/session-results.json format** — emit one object per finding in the canonical schema:
>
> ```json
> {
> "findings": [
> {
> "id": "session-1",
> "skill": "sast-session",
> "severity": "high",
> "title": "Session fixation in POST /login — no session ID regeneration",
> "description": "The login handler at routes/auth.js:42 assigns req.session.userId = user.id without first calling req.session.regenerate(). An attacker who pre-sets a session cookie before the victim logs in will obtain an authenticated session without knowing the victim's credentials.",
> "location": { "file": "routes/auth.js", "line": 42, "column": 3 },
> "remediation": "Wrap the session attribute assignment inside a req.session.regenerate() callback: req.session.regenerate((err) => { req.session.userId = user.id; res.json({ ok: true }); });",
> "exploitability": "reachable",
> "confidence": "high",
> "chain_id": "session-hijack"
> }
> ]
> }
> ```
>
> If no findings were confirmed as VULNERABLE, emit `{ "findings": [] }`.
>
> After writing both output files, delete `sast/session-recon.md` and all `sast/session-batch-*.md` files.
---
## Chain ID Reference
| chain_id | Findings involved | Combined impact |
|---|---|---|
| `session-hijack` | Session fixation or low-entropy ID finding + SESSION_COOKIE_SECURE=False or HttpOnly=False (from sast-cookieflags) | Pre-auth session ID known to attacker + session transmitted over HTTP = full account takeover without brute force |
| `credential-stuffing` | Low-entropy session ID generation + no rate limiting on login endpoint (from sast-ratelimit) | Predictable session IDs + unlimited login attempts = enumerate session IDs and credential stuff simultaneously |
---
## Important Reminders
- Pass the contents of `sast/architecture.md` to every subagent as context.
- Phase 2 batches **must all run in parallel** — do not wait for one batch before launching the next.
- Phase 3 merge **must not start until all Phase 2 batches complete**.
- The most critical check is **authenticate-then-assign without regeneration** — it leads directly to full account takeover.
- In Express, **the assignment must be inside the `req.session.regenerate()` callback**, not before it. Code that calls `regenerate()` after assigning to the session is still vulnerable because the assignment happened on the old session ID.
- Devise (Rails) handles session fixation automatically since version 3.1 — do not flag `sign_in()` calls unless the sessions controller is customized and overrides the default behavior.
- Spring Security's default changed between versions: prior to Spring Security 4.0, the default was `sessionFixation().none()`. Codebases that pin to old Spring Security versions and do not configure `sessionManagement()` explicitly may be vulnerable — classify as `confidence: medium` and note the version dependency.
- `session_regenerate_id()` in PHP **without the `true` argument** keeps the old session data file accessible by its old ID for the remainder of its lifetime. This is a weaker mitigation and should be flagged as `medium` with `conditional` exploitability.
- Do not flag Django's built-in `login()` as vulnerable without first checking whether `cycle_key()` is called in the same handler. Django 4.2+ calls `cycle_key()` internally when `SESSION_COOKIE_SECURE = True` — check the Django version if visible in `requirements.txt` or `pyproject.toml`.
- When in doubt, classify as `confidence: medium` and `exploitability: conditional` rather than dismissing the finding — false negatives in session fixation assessments are worse than false positives.