← back to lessons

When IDOR is saturated/disciplined, PIVOT bug class to SSRF/server-side-fetch - it's the live unmined vein. Plus: the "fetch-a-URL feature" by-design gate (cloud/multi-tenant framing is the cash angle).

Date 2026-06-21. After confirming the IDOR-asymmetry cash vein is exhausted (popular apps saturated by active CVE sweeps; well-engineered fresh apps centrally-guarded), I pivoted bug CLASS to SSRF. FIRST SSRF target (Khoj) yielded a real undisclosed full-read SSRF candidate. The pivot works.

Core lesson: apps are IDOR-disciplined but SSRF-careless

Modern multi-tenant apps invest in a central permission/ACL layer (so IDOR is hard) but route user/LLM-supplied URLs into raw server-side fetchers with NO private-IP/scheme/host guard. The SSRF surface is everywhere AI/ML + web apps fetch: RAG URL-ingestion, web-search/read-webpage tools, webhooks, plugin/integration fetchers, image/file download, OAuth/metadata discovery, "import from URL". When the IDOR triage walks on a target, IMMEDIATELY re-triage the SAME target for SSRF - the guard maturity is class-specific (Khoj has IDOR advisories = IDOR-aware, but fetches raw URLs). - Detector: grep requests/httpx/aiohttp/urllib/urlopen .get(<user_url>) repo-wide; for each, is there a private-IP/metadata/scheme/host guard at the fetch OR a global egress proxy? Usually none. Then: is the URL user/LLM-reachable, and is the response reflected (full-read) vs blind? - Watch the INVERTED guard: Khoj's is_internal_url did not BLOCK internal URLs - it ROUTED them to the direct scraper (still fetched). A function that NAMES internal URLs is not necessarily a guard; read what it DOES with them.

The "fetch-a-URL feature" BY-DESIGN gate (the cap, and the cash framing)

The big triage risk for SSRF in "read this URL / index this page / connect to my server" features: maintainers often INTENTIONALLY support internal fetching (self-hosted Ollama, internal firecrawl, LAN indexing) - so on a SELF-HOSTED single-user instance, internal fetch is arguably intended -> triage-reject. Khoj issues #1114/#1100/#1259 are explicit evidence the authors WANT internal connectivity. - RESOLUTION = the CLOUD / MULTI-TENANT framing: on the vendor's HOSTED multi-tenant instance, a low-priv/any user making the SERVER fetch 169.254.169.254 (cloud metadata) / internal services / other-tenant infra is NOT the self-hoster's intended LAN feature = a real, payable SSRF. Always check: (1) is the vendor's hosted/cloud product in the bounty scope (vs OSS-only)? (2) is metadata/internal reachable from the cloud's network? The cash angle for "fetch-URL" SSRF is the hosted product, not the self-hosted binary. - Severity nuance: 169.254.169.254 is LINK-LOCAL, which Python ipaddress.is_private returns FALSE for -> link-local often slips past naive is_private guards entirely (a bonus bypass). RFC1918/loopback/::1 are the reliable internal-read targets.

Floor still applies (and worked here)

Gate-0 disclosure FIRST by exact sink incl OPEN+CLOSED issues/PRs - did it right on Khoj (the gate I failed twice on RAGFlow). Gate-0 PASS + the by-design caveat -> STAGE with caveats, do NOT live-confirm unprompted (operator weighs scope + by-design first). Saturation-check each SSRF surface too: Open WebUI already has an SSRF advisory (GHSA-c6xv-rcvw-v685, /retrieval/process/web) = that surface partly swept; the popular ones may be saturated on SSRF too.

Next-targets for the SSRF pivot

The fetcher/webhook/plugin/AI surfaces of the IDOR-disciplined apps already triaged this session: Teable (plugin/webhook/AI/attachment - flagged by its triage agent), Dify (url-fetch/RAG), Onyx (web loaders), Documenso (webhooks). Gate-0 saturation-check each first; weigh the by-design gate; frame cloud/multi-tenant for cash.

Generated 2026-07-02 13:15:04 UTC | auto-sync /15min