# Idno Vulnerable to Unauthenticated SSRF via URL Unfurl Endpoint

Unauthenticated SSRF in Idno's URL unfurl endpoint: fabricated API auth headers set the API flag before HMAC check, bypassing CSRF token and enabling internal/metadata fetches.

- Payout: Research
- Severity: critical
- Program: Idno
- Found by: anuraagbaishya
- Disclosed: 2026-03-02
- CVE: CVE-2026-28508
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-fcrh-fqxh-6fx6
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-fcrh-fqxh-6fx6
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:critical tech:php technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:ssrf

**Target:** PHP web app URL unfurl service endpoint (/service/web/unfurl)
**Root cause:** Session::tryAuthUser() calls setIsAPIRequest(true) as soon as X-IDNO-USERNAME and X-IDNO-SIGNATURE headers are present, before HMAC verification, so Actions::validateToken() short-circuits and tokenGatekeeper passes. The unfurl endpoint also lacks gatekeeper() and validates URLs with FILTER_VALIDATE_URL only, permitting private/loopback/metadata addresses.
**Steps:**
1. Send `GET /service/web/unfurl?url=http://localhost:9001/test.html` with headers `X-Requested-With: XMLHttpRequest`, `X-IDNO-USERNAME: x`, `X-IDNO-SIGNATURE: x` (any non-empty values).
2. xhrGatekeeper passes due to X-Requested-With header.
3. tryAuthUser sets is_api_request flag from the two headers before HMAC check; tokenGatekeeper's validateToken returns true immediately.
4. UnfurledUrl::unfurl() passes the URL through FILTER_VALIDATE_URL (accepts localhost/169.254.169.254/10.x) and fetches via Webservice::file_get_contents().
5. Response body is parsed for OpenGraph/mf2 and returned in JSON, leaking internal content.
**How found:** Reporter read the source and noticed the commented-out gatekeeper() with a TODO about logged-out access, then traced tokenGatekeeper -> validateToken -> isAPIRequest and found setIsAPIRequest(true) executed before the HMAC comparison in tryAuthUser.
**Exploitation:** Attacker with no credentials sends a crafted GET with three headers to force the server to fetch arbitrary internal URLs (localhost services, cloud metadata endpoints like 169.254.169.254) and reads the parsed response body, enabling IAM credential theft on IMDSv1, internal port scanning via timing/content, and access to localhost-restricted admin interfaces.
**Escalation:** Chaining metadata service access to steal IAM/OAuth tokens yields cloud account takeover; internal service reach enables Redis/Memcached interaction and admin panel access.
**Insight:** When an auth flow sets a session/request flag (like is_api_request) before verifying credentials, every downstream check that trusts that flag becomes bypassable with fabricated headers; grep for flag-setters preceding HMAC/signature comparisons.

## Proof of concept (copied exactly from the original report)

**curl request triggering SSRF to localhost**

```
curl -s "http://rpi:9090/service/web/unfurl?url=http://localhost:9001/test.html" \
    -H "X-Requested-With: XMLHttpRequest" \
    -H "X-IDNO-USERNAME: x" \
    -H "X-IDNO-SIGNATURE: x"
```

**Internal webserver setup for PoC**

```
python -m http.server --bind 127.0.0.1 9001
Serving HTTP on 127.0.0.1 port 9001 (http://127.0.0.1:9001/) ...
```

