<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ssrf · XB Field Notes</title><link>https://www.xbcatm.com/tags/ssrf/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/ssrf/index.xml" rel="self" type="application/rss+xml"/><item><title>SSRF makes the server open the door</title><link>https://www.xbcatm.com/posts/ssrf-makes-the-server-open-the-door/</link><pubDate>Sun, 05 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/ssrf-makes-the-server-open-the-door/</guid><description>When a server fetches a URL you control, it does so with its own network rights. Server-side request forgery turns that convenience into a way to reach internal hosts.</description><content:encoded>&lt;p&gt;Any feature that takes a user-supplied URL and fetches it server-side runs that fetch with the server&amp;rsquo;s identity, network position and credentials. A thumbnailer, a webhook tester, an importer, a PDF renderer — the moment the target comes from the request, the attacker is not visiting your internal network. Your server is visiting it for them.&lt;/p&gt;
&lt;p&gt;What makes this outrank most injection bugs is reachability. The server sits inside the perimeter: it can open the loopback admin panel, the RFC 1918 services, and the link-local address serving instance credentials. The only thing between an attacker and that network is the code deciding which URLs are acceptable — usually a string comparison.&lt;/p&gt;
&lt;h2 id="the-fetch-carries-the-servers-authority"&gt;The fetch carries the server&amp;rsquo;s authority&lt;/h2&gt;
&lt;p&gt;The classic target is the instance metadata service. On AWS it answers on 169.254.169.254, and &lt;code&gt;GET /latest/meta-data/iam/security-credentials/&lt;/code&gt; returns the instance&amp;rsquo;s IAM role name, followed by that role&amp;rsquo;s temporary credentials. Those credentials are what the instance uses for every AWS API call, so an SSRF that reaches the metadata endpoint does not read a config file — it hands over the instance&amp;rsquo;s cloud identity. OWASP lists the same shape for internal REST interfaces and HTTP-speaking databases that carry no authentication because they were only meant to be reachable from inside.&lt;/p&gt;
&lt;p&gt;The fixture measures both halves. Against a local stand-in it drives the hex form of 127.0.0.1 through a check that inspects the URL string, which reports ALLOWED, and the fetch returns a credential document. Then it measures the hardening: a v2-only instance answers a bare GET with 401, releasing metadata only when the request carries &lt;code&gt;X-aws-ec2-metadata-token&lt;/code&gt;, which the attacker must first obtain with a &lt;code&gt;PUT /latest/api/token&lt;/code&gt;. A single-shot SSRF cannot complete that handshake.&lt;/p&gt;
&lt;h2 id="a-url-is-not-an-address"&gt;A URL is not an address&lt;/h2&gt;
&lt;p&gt;String checks fail because the string is not the address the client connects to. The same loopback address has many spellings: &lt;code&gt;http://2130706433/&lt;/code&gt; is decimal, &lt;code&gt;http://0x7f000001/&lt;/code&gt; is hex, &lt;code&gt;http://127.1/&lt;/code&gt; is a short form, &lt;code&gt;http://[::1]/&lt;/code&gt; covers the IPv6 side. The fixture resolves every one through getaddrinfo and gets 127.0.0.1 back, while a substring check for &lt;code&gt;127.0.0.1&lt;/code&gt; allows all of them.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;@&lt;/code&gt; is worse, because it makes an allowlist pass. In &lt;code&gt;http://trusted.example@169.254.169.254/&lt;/code&gt; the userinfo is &lt;code&gt;trusted.example&lt;/code&gt; and the host is &lt;code&gt;169.254.169.254&lt;/code&gt;. RFC 3986 separates them, but a regex that captures everything after the scheme and tests for the allowlisted name sees the name and approves. The fixture&amp;rsquo;s regex allowlist reports ALLOWED for that URL while urlsplit reports the real host. Redirects and DNS rebinding are the same idea on a time axis: the address is validated, then something changes before the connection.&lt;/p&gt;
&lt;h2 id="validate-after-resolution-then-pin-the-connection"&gt;Validate after resolution, then pin the connection&lt;/h2&gt;
&lt;p&gt;The order matters more than the list. Parse with a real parser, allowlist the scheme, host and port, resolve the hostname, and classify every address that comes back — rejecting loopback, private, link-local, reserved and multicast ranges. Then connect to the address you validated rather than resolving again, or a rebinding name returns a public address to the check and a private one to the socket. The fixture&amp;rsquo;s resolve-then-classify function rejects all seven probe URLs, including the ones the string checks allowed.&lt;/p&gt;
&lt;p&gt;Redirects need the same treatment on every hop: disable them, or re-run the guard on each Location before the client requests it. IMDSv2 is the second layer, since it makes a bare GET useless, and a network policy stopping the workload from reaching 169.254.169.254 is the third. None substitutes for the others: the allowlist decides intent, address classification decides reachability, the egress rule decides possibility.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The first failure is a fetcher that trusts the string. It presents as a 200 where you expected a rejection, or a body that looks like neither your application nor a normal error: a credential document, an admin page, a database&amp;rsquo;s REST reply. In the fixture the naive check reports ALLOWED for the hex loopback URL and the fetch returns the credential JSON — the attack in two lines, no exploit payload, just a URL the guard misread.&lt;/p&gt;
&lt;p&gt;The second failure is the guard that validates once. A redirect on an allowlisted host, or a name whose answer changes between check and connect, walks past a single pre-flight validation. The fixture follows the redirect hop and gets the metadata body back, while a client re-validating each hop refuses it. The blast radius is worst when the target is an internal control plane with no authentication, precisely because it believed only localhost could reach it.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Probe your own fetcher with the shapes a string check misses:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -s -o /dev/null -w &amp;#39;allowlisted -&amp;gt; %{http_code}\n&amp;#39; &amp;#39;https://app.example.com/api/fetch?url=https://cdn.example.com/logo.png&amp;#39;
curl -s -o /dev/null -w &amp;#39;imds -&amp;gt; %{http_code}\n&amp;#39; &amp;#39;https://app.example.com/api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/&amp;#39;
curl -s -o /dev/null -w &amp;#39;decimal -&amp;gt; %{http_code}\n&amp;#39; &amp;#39;https://app.example.com/api/fetch?url=http://2130706433:8080/admin&amp;#39;
curl -s -o /dev/null -w &amp;#39;userinfo -&amp;gt; %{http_code}\n&amp;#39; &amp;#39;https://app.example.com/api/fetch?url=http://trusted.example@169.254.169.254/&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first must succeed, because a guard that blocks everything is an outage rather than a guard. The other three must come back 4xx with no body from the target; a 200 means the fetcher decided from the URL string. A 200 carrying a credential document, an internal page or an echoed connection error is a confirmed SSRF.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-community/attacks/Server_Side_Request_Forgery"&gt;OWASP: Server Side Request Forgery&lt;/a&gt;, what the attacker reaches and why metadata endpoints matter, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"&gt;OWASP: Server-Side Request Forgery Prevention Cheat Sheet&lt;/a&gt;, allowlisting, resolve-then-connect and redirect handling, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html"&gt;AWS: Use the Instance Metadata Service&lt;/a&gt;, the IMDSv2 token handshake and the v2-only option, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc3986.html"&gt;RFC 3986: URI Generic Syntax&lt;/a&gt;, the authority, userinfo and host components, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>