Skip to content
Field Value
Platform PortSwigger Web Security Academy
Type SSRF — Whitelist Bypass via URL Parsing Confusion
Difficulty Expert
Objective Bypass a whitelist SSRF filter to reach http://localhost/admin and delete carlos

SSRF with Whitelist-Based Input Filter — Writeup


Initial Observation

Intercepting the stock check request:

Screenshot
stockApi=http://stock.weliketoshop.net:8080/product/stock/check?productId=1%26storeId=1

Trying stockApi=http://localhost/admin:

Response: External stock check host must be stock.weliketoshop.net

Screenshot

Whitelist — only stock.weliketoshop.net is accepted as the host. Every IP obfuscation technique from the blacklist lab returns the same error. We need to construct a URL that the filter reads as stock.weliketoshop.net but the HTTP client resolves differently.

Testing Credential Injection

URLs support credentials before the hostname using the @ character: http://user:password@host. The part before @ is the userinfo — the actual host starts after it. Some filters check the full URL string for a match rather than properly parsing the host component, which means we might be able to sneak the legitimate domain into the userinfo section while pointing the actual host elsewhere.

Trying:

stockApi=http://localhost:[email protected]

Response: 500 Internal Server Error — Could not connect to external stock check service

Screenshot

Different error — the filter passed. The server tried to connect to stock.weliketoshop.net with localhost:80 as credentials, which doesn't work. But the filter accepted it, which confirms the filter is matching stock.weliketoshop.net anywhere in the string rather than parsing the host properly.

We need the HTTP client to connect to localhost — but the @ makes localhost:80 the userinfo and stock.weliketoshop.net the actual host. We need to flip that.

Double-Encoding the Fragment — The Actual Bypass

The # character in a URL starts a fragment identifier. Everything after # is the fragment — it's never sent to the server. So http://localhost#[email protected] would be parsed as:

The filter would see stock.weliketoshop.net in the fragment and might accept it. But the problem is that # gets stripped by the browser or URL parser before it reaches the application — the fragment never makes it to the server.

The fix: double-encode the #. A single # is %23. If we URL-encode the % as well, we get %2523. When the filter processes the URL it decodes once — %2523 becomes %23, which is the literal string %23, not a fragment. The filter sees no # and no fragment, so it checks the full string and finds stock.weliketoshop.net as part of the credentials — passes.

When the server's HTTP client makes the actual request, it decodes again — %23 becomes #. Now the URL is parsed properly: localhost:80 is the host, #[email protected] is the fragment, and the request goes to localhost.

stockApi=http://localhost:80%[email protected]
Screenshot

The admin panel appears in the response. The filter saw stock.weliketoshop.net after the @ (because %2523 wasn't decoded to a # yet). The HTTP client decoded %2523%23#, which turned @stock.weliketoshop.net into a fragment, making localhost:80 the actual destination.


Accessing Admin and Deleting Carlos

stockApi=http://localhost:80%[email protected]/admin

Admin panel loads. Navigating to the delete endpoint:

stockApi=http://localhost:80%[email protected]/admin/delete?username=carlos
Screenshot
Screenshot

Imagine carlos being mad with support cuz his account gets deleted on and on

Resources