<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ali Haider</title>
    <description>The latest articles on DEV Community by Ali Haider (@alihaider_dev).</description>
    <link>https://dev.to/alihaider_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4158670%2Fa534ff3d-7271-42d4-ad36-518f51f4a36c.png</url>
      <title>DEV Community: Ali Haider</title>
      <link>https://dev.to/alihaider_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alihaider_dev"/>
    <language>en</language>
    <item>
      <title>When Your Own Server Becomes the Attacker</title>
      <dc:creator>Ali Haider</dc:creator>
      <pubDate>Sat, 03 Oct 2026 00:17:53 +0000</pubDate>
      <link>https://dev.to/alihaider_dev/when-your-own-server-becomes-the-attacker-jgb</link>
      <guid>https://dev.to/alihaider_dev/when-your-own-server-becomes-the-attacker-jgb</guid>
      <description>&lt;p&gt;In March 2019, a former Amazon engineer made a request to a Capital One server. Not a login, not a password guess. She asked the server to fetch a URL for her.&lt;/p&gt;

&lt;p&gt;The server said yes.&lt;/p&gt;

&lt;p&gt;That one request walked out with temporary AWS credentials, and those credentials unlocked the personal data of &lt;strong&gt;more than 100 million people&lt;/strong&gt;. Social security numbers, bank account details, credit applications. One of the largest financial breaches in US history, and at the bottom of it was a bug so ordinary that you have probably written the ingredients for it yourself.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;SSRF&lt;/strong&gt;, and I want to show you exactly how something this mundane turns into something this catastrophic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole idea in one line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SSRF (Server-Side Request Forgery)&lt;/strong&gt; is when you trick a server into making a request &lt;em&gt;for you&lt;/em&gt;, to somewhere you could never reach on your own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. You don't break in. You get the server to walk in for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think about who you trust
&lt;/h2&gt;

&lt;p&gt;Picture a secure office. You're a stranger in the lobby. The server room, the records vault, the executive offices, all locked, all off-limits. Security won't let you past the front desk.&lt;/p&gt;

&lt;p&gt;But the receptionist runs errands. &lt;em&gt;"Could you grab the file from room 237?"&lt;/em&gt; Sure. They walk back, fetch it, hand it over.&lt;/p&gt;

&lt;p&gt;So you try your luck: &lt;em&gt;"Could you bring me whatever's in the server room?"&lt;/em&gt; And because the receptionist has access, and you asked politely, they do.&lt;/p&gt;

&lt;p&gt;You never touched the lock. The receptionist did the walking. &lt;strong&gt;That receptionist is your server&lt;/strong&gt;, and SSRF is the art of asking it to fetch things it shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature that becomes the bug
&lt;/h2&gt;

&lt;p&gt;Here's what makes SSRF sneaky: it almost never looks like a vulnerability. It looks like a &lt;strong&gt;feature request&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Let users add a profile picture by URL."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Check stock levels from the warehouse's internal API."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Generate a PDF preview of any link."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Fire a webhook to the customer's endpoint."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those ends up as some version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userControlledUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer who wrote that line was picturing a normal URL. An image. A warehouse API. A customer's webhook. What they weren't picturing was someone typing:&lt;br&gt;
&lt;code&gt;http://localhost/admin&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And the server, having no opinion about where it's pointed, fetches its own admin panel, the one that's only supposed to be reachable from inside, and hands the attacker the response. The firewall guarding that panel never even got a say, &lt;strong&gt;because the request came from inside the house.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When the server starts scanning for you
&lt;/h2&gt;

&lt;p&gt;Reaching the admin panel is just the opening move. The deeper problem is that the server lives inside a &lt;strong&gt;private network you can't see&lt;/strong&gt; from the outside.&lt;/p&gt;

&lt;p&gt;So you make it look around.&lt;/p&gt;

&lt;p&gt;Point it at &lt;code&gt;192.168.0.1&lt;/code&gt;, then &lt;code&gt;.2&lt;/code&gt;, then &lt;code&gt;.3&lt;/code&gt;, and watch how the responses differ. A connection that refuses instantly means nothing's there. One that hangs, or answers, means something is. Walk the whole range and the server quietly &lt;strong&gt;maps out the internal network for you&lt;/strong&gt;, which machines exist, which ports are open, where the interesting things live.&lt;/p&gt;

&lt;p&gt;You've turned the victim's own server into a scanner aimed at the victim's own network.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that ended Capital One
&lt;/h2&gt;

&lt;p&gt;Now the finale, the move that turns &lt;em&gt;"interesting bug"&lt;/em&gt; into &lt;em&gt;"front-page breach."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every server running on &lt;strong&gt;AWS, Google Cloud, or Azure&lt;/strong&gt; can reach a special internal address: &lt;code&gt;169.254.169.254&lt;/code&gt;. It's the &lt;strong&gt;metadata service&lt;/strong&gt;, a little endpoint the machine uses to learn about itself. Handy for the server. Devastating in the wrong hands, because on a misconfigured instance it will also hand over the server's &lt;strong&gt;temporary cloud credentials&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the attacker points the SSRF here:&lt;br&gt;
&lt;code&gt;http://169.254.169.254/latest/meta-data/iam/security-credentials/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;and the server, asked politely, returns its own keys to the kingdom. With those, you're no longer poking at one app. &lt;strong&gt;You're inside the cloud account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole Capital One story in one request: a URL field that trusted its input, a server that could reach the metadata endpoint, and 100 million records out the door.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to not be the next case study
&lt;/h2&gt;

&lt;p&gt;This is the part most write-ups skip, so here's the version I'd actually want on a code review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Allowlist, don't blocklist.&lt;/strong&gt;&lt;br&gt;
The instinct is to block the bad addresses. Don't. Attackers will always find a spelling you didn't think of: &lt;code&gt;127.0.0.1&lt;/code&gt; can be written as &lt;code&gt;0x7f000001&lt;/code&gt;, or &lt;code&gt;2130706433&lt;/code&gt;, or hidden behind a domain that quietly resolves to an internal IP. Blocklists leak. Instead, decide the exact destinations your feature is allowed to hit, and refuse everything else. If the stock checker only ever needs one internal API, that's the only thing it should be able to reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check the resolved IP, not the string.&lt;/strong&gt;&lt;br&gt;
If you must allow user URLs, resolve the hostname first and inspect the actual IP it points to, then block the private ranges (&lt;code&gt;127.x&lt;/code&gt;, &lt;code&gt;169.254.x&lt;/code&gt;, &lt;code&gt;10.x&lt;/code&gt;, &lt;code&gt;192.168.x&lt;/code&gt;, &lt;code&gt;172.16–31.x&lt;/code&gt;). A domain like &lt;code&gt;my-innocent-site.com&lt;/code&gt; can resolve straight to &lt;code&gt;169.254.169.254&lt;/code&gt; if the attacker owns the DNS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Kill the exotic schemes.&lt;/strong&gt;&lt;br&gt;
Allow &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt;, nothing else. &lt;code&gt;file://&lt;/code&gt; reads local files. &lt;code&gt;gopher://&lt;/code&gt; can forge raw requests to internal services like Redis. If your feature doesn't need them, they're pure downside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. On AWS, enforce IMDSv2.&lt;/strong&gt;&lt;br&gt;
The newer metadata service requires a session token before it hands anything over, which shuts down the exact move that hit Capital One. It's close to a one-setting fix for the worst version of this bug. Turn it on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Assume one layer fails.&lt;/strong&gt;&lt;br&gt;
The app server shouldn't be able to reach your sensitive internal systems in the first place. Segment the network so that even a successful SSRF runs into a wall instead of a vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  One question before you close this tab
&lt;/h2&gt;

&lt;p&gt;Go find the place in your codebase where the server fetches a URL it didn't fully choose. The image importer, the webhook, the link preview, the PDF renderer. There's almost always one.&lt;/p&gt;

&lt;p&gt;Now ask it two things: &lt;strong&gt;is the destination allowlisted, and can it reach &lt;code&gt;169.254.169.254&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't like the answers, you've just found the same bug that cost Capital One 100 million records, sitting quietly in your own repo, still looking like a feature.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>cybersecurity</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
