Skip to content
Threats Guide

secure_session.py

Secure server-side session manager

  • prevention
  • python
  • 95 lines

Run security tools only on systems you own or have written permission to test.

Run it

From the repository root, after pip install -r tools/requirements.txt
cd 02-web-application-security/session-hijacking/prevention
python3 secure_session.py --help

Source

View on GitHub
#!/usr/bin/env python3"""Secure server-side session managerLocation: 02-web-application-security/session-hijacking/prevention/secure_session.pyDefences against session hijacking and fixation, in one small class:  * 256-bit random session ids from the OS CSPRNG (`secrets`), never guessable  * only a SHA-256 hash of each id is stored, so a leaked session table cannot be replayed  * a NEW id is issued at login (`rotate`), which defeats session fixation  * idle timeout and absolute timeout  * the session is tied to a hash of the User-Agent (and optionally the client IP network)  * logout and "log out everywhere" really destroy the server-side session  * cookie attributes are generated for you: __Host- prefix, Secure, HttpOnly, SameSiteThis is an in-memory teaching version; production code should keep the same logic in Redis or adatabase, and always serve over HTTPS.""" import hashlibimport secretsimport time COOKIE_NAME = "__Host-session"          # __Host- forces Secure, Path=/ and no Domain attribute  def _hash(value: str) -> str:    return hashlib.sha256(value.encode()).hexdigest()  class SessionManager:    def __init__(self, idle_timeout=900, absolute_timeout=28800, bind_ip_prefix=False):        self.idle_timeout = idle_timeout        self.absolute_timeout = absolute_timeout        self.bind_ip_prefix = bind_ip_prefix        self._sessions = {}                 # sha256(session id) -> record     @staticmethod    def _ip_prefix(ip):        return ".".join(ip.split(".")[:3]) if "." in ip else ip.rsplit(":", 4)[0]     def create(self, user_id, user_agent, ip, now=None):        session_id = secrets.token_urlsafe(32)        now = now if now is not None else time.time()        self._sessions[_hash(session_id)] = {            "user": user_id, "created": now, "last_seen": now,            "ua": _hash(user_agent), "net": self._ip_prefix(ip),        }        return session_id     def validate(self, session_id, user_agent, ip, now=None):        """Return the user id, or None if the session is unknown, expired or looks stolen."""        key = _hash(session_id or "")        record = self._sessions.get(key)        if record is None:            return None        now = now if now is not None else time.time()        expired = now - record["last_seen"] > self.idle_timeout or now - record["created"] > self.absolute_timeout        stolen = record["ua"] != _hash(user_agent) or (self.bind_ip_prefix and record["net"] != self._ip_prefix(ip))        if expired or stolen:            del self._sessions[key]          # fail closed: destroy it            return None        record["last_seen"] = now        return record["user"]     def rotate(self, old_session_id, user_agent, ip, now=None):        """Call after login or privilege change: returns a fresh id and destroys the old one."""        record = self._sessions.pop(_hash(old_session_id or ""), None)        user = record["user"] if record else None        return self.create(user, user_agent, ip, now) if user is not None else None     def destroy(self, session_id):        self._sessions.pop(_hash(session_id or ""), None)     def destroy_all(self, user_id):        for key in [k for k, v in self._sessions.items() if v["user"] == user_id]:            del self._sessions[key]     def cookie_header(self, session_id):        return (f"{COOKIE_NAME}={session_id}; Path=/; Secure; HttpOnly; SameSite=Lax; "                f"Max-Age={self.absolute_timeout}")  if __name__ == "__main__":    manager = SessionManager(idle_timeout=900)    sid = manager.create("alice", "Mozilla/5.0", "203.0.113.7", now=0)    print("valid session            :", manager.validate(sid, "Mozilla/5.0", "203.0.113.7", now=60))    print("different browser (theft):", manager.validate(sid, "curl/8.0", "203.0.113.7", now=61))    sid = manager.create("alice", "Mozilla/5.0", "203.0.113.7", now=0)    new_sid = manager.rotate(sid, "Mozilla/5.0", "203.0.113.7", now=10)    print("old id after rotation    :", manager.validate(sid, "Mozilla/5.0", "203.0.113.7", now=11))    print("new id works             :", manager.validate(new_sid, "Mozilla/5.0", "203.0.113.7", now=12))    print("after idle timeout       :", manager.validate(new_sid, "Mozilla/5.0", "203.0.113.7", now=12 + 1000))    print("cookie                   :", manager.cookie_header("<id>"))