- Status Closed
-
Assigned To
cbay - Private
Opened by Bores - 25.08.2026
Last edited by cbay - 26.08.2026
FS#462 - SSRF guard does not cover your own infrastructure, which uses global IPv6
Summary
The address guard added after FS#401 decides on private versus public address ranges. It refuses RFC1918, loopback, link-local and ULA. Your own internal hosts are not in any of those ranges: they sit in your global allocation 2a00:b6e0::/32, which the guard treats as ordinary public space and allows. From inside, www, admin, api and webmail.alwaysdata.com all resolve to 2a00:b6e0:1:84:1::1, which is overlord-core.paris1.alwaysdata.com, and none of those names publish an AAAA record publicly. So the guard leaves the hosts it most needs to protect in its permitted class, and no parser trick is required to reach them. This is a different root cause from FS#461 , which is about how IPv4 literals are parsed rather than which ranges the guard treats as safe.
Vulnerable asset
https://admin.alwaysdata.com/site/application/script/<id>/, field script_upstream_uri, fetched by POST …/update_script/.
The fetching host answers from 2a00:b6e0:1:84:1::1 and identifies itself as python-requests/2.32.5.
Root cause
The guard implements a deny list of private ranges rather than an allow list of destinations the fetcher is permitted to reach. That model only works when everything sensitive is inside those private ranges. On your platform it is the other way round: tenant services are given ULA addresses, which the guard blocks, while your own management hosts use globally-routable addresses, which it allows. The guard therefore protects tenant-to-tenant traffic and leaves your own infrastructure exposed to it.
Attack path
Everything below was measured from a script object on my own account and from my own shell on ssh2. I did not connect to any of your internal services.
First, the guard does block ULA, and I proved that with a service of my own rather than by inference. I ran python3 -m http.server 8199 on my account, which binds to my tenant address fd00::7:89a5, serving a file containing a marker. Note that ip -6 addr does not list that address, only the node's 2a00:b6e0:1:50:1::1, so the bound address has to be read from ss -lnt. From my own shell the URL works:
$ curl http://[fd00::7:89a5]:8199/marker.txt SSRF-RANGE-PROOF-2a00b6e0
Submitting that same URL as script_upstream_uri returns in 0.632702s, repeated at 0.590323s, and leaves the installation script field at its baseline, so nothing was fetched.
That the fetcher can actually route to that address is not something you have to take from me: it is in FS#460 , which you fixed and paid. That report records direct http://[fd00::7:89a5]:8080/ 0.601s refused by guard alongside redirect → http://[fd00::7:89a5]:8080/ 20.381s connection attempted, timed out. Same address, same fetcher. So a sub-second answer with an empty field is the guard refusing, not the network failing.
Second, the guard allows global IPv6, and there the fetch really happens. Pointing the field at example.com's AAAA record returned the origin's response body, which was stored in the installation script field where I read it back:
script_upstream_uri = http://[2606:4700:10::6814:179a]/ 0.595333s
What landed in the installation script field was the origin's own error page: "400 Bad Request" as both the title and the heading, ending with a footer line reading "cloudflare". I have written its text out rather than pasting the markup, since raw tags do not survive this tracker's formatting. The point is not the page itself but that a body came back from a global IPv6 address and was stored where I can read it.
Third, your own hosts are in that allowed class. From my shell on ssh2:
$ for h in www admin api webmail security; do getent hosts $h.alwaysdata.com; done 2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com www.alwaysdata.com 2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com admin.alwaysdata.com 2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com api.alwaysdata.com 2a00:b6e0:1:84:1::1 overlord-core.paris1.alwaysdata.com webmail.alwaysdata.com 2a00:b6e0:1:210:1::1 security.alwaysdata.com
From the public internet the same names resolve to 185.31.40.5 and publish no AAAA at all, so those v6 addresses are an internal view rather than something the public is directed to.
Putting the three together: an address in 2a00:b6e0::/32 is accepted by the guard, an address the fetcher can reach is fetched and its body is handed back to me, and your own management hosts live in 2a00:b6e0::/32.
Impact
Any HTTP service on your internal hosts is reachable from this fetcher and its response body is readable by the customer who submitted the URL, with no notation trick as in FS#461 and no redirect as in FS#460 . The guard does not stand between a customer and your infrastructure, only between a customer and other tenants.
This matters more than a single bypass because the guard is currently the whole protection on this feature. The FS#460 fix removed redirect following, so there is no second control behind it.
Your own tracker sets the context for what class of host is in that permitted range: FS#415 describes overlord-core as the core management server for the platform. That is the same host this fetcher runs on and the same host the internal names above resolve to.
I stopped at demonstrating the address class. Following the SSRF guidance in your rules I did not connect to any internal service, so I cannot tell you what is listening there or what could be retrieved. I am happy to demonstrate that on request or in a development environment.
Suggested fix
Reverse the model. Rather than denying a list of private ranges, deny everything and allow only what the feature legitimately needs, which for an installation script source is public HTTP outside your own networks. At a minimum, add your own allocations, including 2a00:b6e0::/32 and any other prefix your infrastructure uses, to the refused set alongside the private ranges, and apply the check to the resolved address rather than to the submitted string.
It is also worth deciding whether this fetcher needs to run on the management host at all. Moving it to an egress-restricted worker would make the guard a second line of defence rather than the only one.
Testing notes
All testing used a script object on my own account, which has been deleted, and a listener I ran on my own hosting account, which has been stopped and removed. Manual requests only, no automated scanner. I did not connect to any of your internal services: the only destinations fetched were example.com over IPv6 and my own hosting account. The DNS observations come from ordinary getent lookups on my own shell.
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
poc.sh
Hello,
The fix from https://security.alwaysdata.com/task/461 should have automatically fixed this one too.
Kind regards,
Cyril