It was one of those clean, modern apps — polished UI, fast APIs, nothing obviously broken. The kind where you spend an hour and feel like everything important is already locked down.
I was mostly poking around features.
That's when I saw it.
The Feature That Looked Too Normal
There was a URL preview option.
You paste a link, and the app fetches metadata title, image, description.
Typical request:
POST /api/preview
{
"url": "https://example.com"
}Response included:
- page title
- description
- preview image
Standard stuff.
But anything that makes the server fetch a URL… is worth slowing down for.
First Check (Nothing Interesting)
I sent something simple:
"url": "https://google.com"Worked fine.
Then tried an invalid domain:
"url": "https://nonexistent-test-xyz.com"It returned a timeout error.
That's good.
It means the server is actually making the request — not just validating format.
The Usual SSRF Test
Next step:
"url": "http://127.0.0.1"Response:
Request blocked
Okay, expected.
Tried:
"url": "http://localhost"Same result.
So they were filtering internal addresses.
At this point, it looks "secure enough."
But filters are usually where things break.
Small Observation That Changed Direction
I noticed something weird in the error messages.
When the request failed, it sometimes showed:
"Could not fetch URL"
And sometimes:
"Invalid URL"
That difference matters.
One means validation failed. The other means the request was actually attempted.
So I tried playing with formats.
Bypassing the Filter
Instead of plain localhost, I used decimal encoding:
"url": "http://2130706433"Same as 127.0.0.1.
This time, the response changed.
No "blocked" message.
Just a generic fetch error.
That's progress.
Confirming SSRF
I pointed it to a server I controlled.
"url": "http://my-server.com"Got a hit in my logs.
That confirmed:
The server is making outbound requests based on my input
Now it's real SSRF.
Moving Toward Internal Access
Now the goal was simple:
Can I reach internal services?
Tried again with encoded localhost:
"url": "http://2130706433:80"Response took longer this time.
Different behavior again.
That usually means something is responding internally.
The Risky Step (But Worth It)
Most cloud environments expose metadata on:
http://169.254.169.254So I tried:
"url": "http://169.254.169.254/latest/meta-data/"No direct response in the app.
But timing changed again.
So I narrowed it down.
Extracting Something Real
I requested a specific endpoint:
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"This time, part of the response actually came back in the preview.
Not clean but enough.
That's when it hit:
I'm pulling data from the instance metadata
Why This Was a Big Deal
Metadata endpoints can expose:
- IAM roles
- temporary credentials
- access tokens
With that, you're not just inside the app.
You're inside their cloud environment.
That's a completely different level of impact.
What Made This Possible
The app had protections.
But they were surface-level:
- blocking
127.0.0.1 - blocking
localhost
But not:
- encoded IPs
- alternative formats
- deeper validation
Classic case of blacklist-based filtering.
The Report
I didn't just say "SSRF exists."
I showed:
- external interaction (my server logs)
- internal access attempts
- metadata endpoint behavior
- potential cloud impact
Because SSRF without impact often gets downgraded.
SSRF with cloud exposure doesn't.
𝒯𝒶𝓃𝓋𝒾 ♡