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 ...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add mstfknn/sast-skills --skill sast-session --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Sast Session?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mstfknn-sast-session-sast-skills)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!