<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 27Rafa</title>
    <description>The latest articles on DEV Community by 27Rafa (@27levi).</description>
    <link>https://dev.to/27levi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4130397%2F368c1e3a-9018-4daf-a155-96f7750ece42.jpg</url>
      <title>DEV Community: 27Rafa</title>
      <link>https://dev.to/27levi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/27levi"/>
    <language>en</language>
    <item>
      <title>I built a tool to test whether logout actually invalidates sessions</title>
      <dc:creator>27Rafa</dc:creator>
      <pubDate>Thu, 17 Sep 2026 19:25:10 +0000</pubDate>
      <link>https://dev.to/27levi/i-built-a-tool-to-test-whether-logout-actually-invalidates-sessions-3eea</link>
      <guid>https://dev.to/27levi/i-built-a-tool-to-test-whether-logout-actually-invalidates-sessions-3eea</guid>
      <description>&lt;p&gt;When you click "logout," you expect your session to be dead. Sometimes it isn't.&lt;/p&gt;

&lt;p&gt;The server clears the cookie in your browser. The UI redirects you to the login page. Everything looks right. But the session token is still valid server-side — and anyone who captured it before you logged out can keep using it.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;CWE-613: Insufficient Session Expiration&lt;/strong&gt;. It's the bug that shows up in every security review and almost never gets tested, because verifying it by hand takes 20 minutes per app in Burp Suite.&lt;/p&gt;

&lt;p&gt;I got tired of doing it by hand. So I built &lt;code&gt;ghostsession&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Takes one HAR file containing a full session — login, activity, logout — and:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the logout request&lt;/li&gt;
&lt;li&gt;Executes it&lt;/li&gt;
&lt;li&gt;Replays every authenticated GET/HEAD endpoint using the &lt;strong&gt;old&lt;/strong&gt; session cookie&lt;/li&gt;
&lt;li&gt;Reports which endpoints still return authenticated data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any do, the session was never invalidated. Anyone holding that old cookie still has the account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo
&lt;/h2&gt;

&lt;p&gt;I built a deliberately vulnerable local server. The &lt;code&gt;/logout&lt;/code&gt; endpoint sends a &lt;code&gt;Set-Cookie&lt;/code&gt; header that tells the browser to forget the session — but the server keeps the token alive in memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./ghostsession.py session.har
&lt;span class="go"&gt;
[*] Parsed 8 entries from session.har
&lt;/span&gt;&lt;span class="gp"&gt;[*] Logout found at entry #&lt;/span&gt;6: POST http://127.0.0.1:8001/logout
&lt;span class="go"&gt;[*] Old session cookie: session=2437dcdfe9bca801c35746c463db90f3...
[*] 5 authenticated endpoint(s) to replay after logout

[*] Executing logout: POST http://127.0.0.1:8001/logout
[+] Logout responded: 200
[*] Replaying 5 endpoint(s) with OLD cookie...

----------------------------------------------------------------------
  Target          : http://127.0.0.1:8001
  Logout request  : http://127.0.0.1:8001/logout
  Session cookie  : session=2437dcdfe9bca801c35746c463db90f3...
  Endpoints tested: 5
  Vulnerable      : 5
----------------------------------------------------------------------

  [!] VULNERABLE — session survives logout

    [CRITICAL] GET http://127.0.0.1:8001/me
&lt;/span&gt;&lt;span class="gp"&gt;          status: 200 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;200   similarity: 1.0
&lt;span class="go"&gt;
    [CRITICAL] GET http://127.0.0.1:8001/api/settings
&lt;/span&gt;&lt;span class="gp"&gt;          status: 200 -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;200   similarity: 1.0
&lt;span class="go"&gt;
  CWE-613: Insufficient Session Expiration
  OWASP  : WSTG-SESS-06 / WSTG-SESS-07
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five endpoints still returned full account data after logout. The bug is unambiguous: the "logout" button is cosmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is under-tested
&lt;/h2&gt;

&lt;p&gt;Testing it manually means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in as the user&lt;/li&gt;
&lt;li&gt;Save the session cookie&lt;/li&gt;
&lt;li&gt;Capture every request the app makes&lt;/li&gt;
&lt;li&gt;Click logout&lt;/li&gt;
&lt;li&gt;Replay every captured request with the saved cookie&lt;/li&gt;
&lt;li&gt;Eyeball each response to see which ones still work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Doing that across 20 endpoints in Burp takes an hour. Multiply by every app you review. Most people just check whether the cookie got cleared and move on.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ghostsession&lt;/code&gt; automates the whole loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one design choice that matters
&lt;/h2&gt;

&lt;p&gt;Everything is pure Python standard library. No &lt;code&gt;pip install&lt;/code&gt;. No Go binary. No Burp extension. No Docker. It's one file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://raw.githubusercontent.com/277levi/ghostsession/main/ghostsession.py
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ghostsession.py
./ghostsession.py session.har
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole install. It runs on any box with Python 3.8+, including locked-down pentest jump boxes where you can't install anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detection logic
&lt;/h2&gt;

&lt;p&gt;Three things had to be right for this to produce signal instead of noise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding the logout request.&lt;/strong&gt; Ghostsession matches URLs against &lt;code&gt;/logout&lt;/code&gt;, &lt;code&gt;/signout&lt;/code&gt;, &lt;code&gt;/api/auth/logout&lt;/code&gt;, &lt;code&gt;/sessions/destroy&lt;/code&gt;, and similar patterns. It prefers POST when there are multiple matches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extracting the old cookie.&lt;/strong&gt; It walks backward through the HAR from the logout entry, picks the session cookie off the last authenticated request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detecting a real leak.&lt;/strong&gt; Same HTTP status before and after is not enough. A logout that correctly returns a &lt;code&gt;200&lt;/code&gt; "please log in" page would look identical at the status level. So the tool also requires the response body to be at least 70% similar, using Jaccard similarity on 4-gram shingles. That catches the case where the server quietly served a login page instead of the account data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;Honest limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only tests read-only methods (GET/HEAD). Post-logout &lt;strong&gt;write&lt;/strong&gt; testing is planned for v0.2.&lt;/li&gt;
&lt;li&gt;Assumes the HAR contains a logout request. If you didn't capture one, the tool errors out.&lt;/li&gt;
&lt;li&gt;Doesn't test session fixation — only invalidation.&lt;/li&gt;
&lt;li&gt;If the app rotates the session cookie on logout &lt;em&gt;and&lt;/em&gt; invalidates the old one, ghostsession correctly reports "OK."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exit codes for CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 = session properly invalidated
1 = session survived logout (vulnerable)
2 = fatal error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it into a security pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./ghostsession.py session.har &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"CWE-613 found"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/277levi/ghostsession" rel="noopener noreferrer"&gt;https://github.com/277levi/ghostsession&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Pure stdlib. Issues and PRs welcome.&lt;/p&gt;

&lt;p&gt;I built this because I got tired of checking the same bug by hand. If you do security reviews, I hope it saves you the same hour.&lt;/p&gt;

&lt;p&gt;If you have a HAR file from a recent session and want to see whether the app got it right — run it and let me know what you find. Genuinely curious how common this is in the wild.&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
