Server-Side Request Forgery is one of the most fluent terms in application security. It has an OWASP Top-10 slot (A10:2021), a CWE (CWE-918), a bug-bounty price list, and a ready-made mental picture: a server is tricked into sending a request somewhere it should not — the cloud metadata endpoint, an internal admin service, an RFC 1918 host. Everyone knows what an SSRF looks like.
That fluency is exactly the problem. SSRF tells us what the server did. It often does not tell us why the attack succeeded. The term has become broad enough to describe different causal mechanisms as though they were one threat — and a control strategy built on the name inherits that blur.
This post uses SSRF as a worked example of a larger claim: technique names are not taxonomies. One fluent industry term does not equal one ontologically consistent threat.
The causal split
TLCTC classifies an attack step by the generic vulnerability it targets, not by the technique label or the downstream effect (Axiom I; v2.5 core §2). Applied to SSRF, the question is never “did the server make a request it should not have?” — it is “what made that request possible?” There are two structurally different answers.
| What happened? | Why could it happen? | TLCTC |
|---|---|---|
| The server intentionally has a fetch / proxy / relay function; the attacker points it somewhere dangerous | A legitimate capability is abused within its designed scope | #1 Abuse of Functions |
| The server makes a request only because a software implementation defect was triggered | A code flaw is exploited | #2 Exploiting Server |
The v2.5 boundary tests state the rule in both directions. Under #1: if an implementation flaw is required → #2 or #3. Under #2: if behaviour is achieved without an implementation flaw (pure feature/config misuse) → #1. The glossary compresses this into one decision question:
Did the abused capability execute as designed, just for a forbidden target?
A useful way to run the test: would the attack still work if the functionality were perfectly implemented? If yes — because the capability itself is being misused — it is #1. If no — because an implementation defect is necessary — it is #2 (or #3 when the flawed component is in a client role, per R-ROLE).
The word SSRF collapses “the server was tricked into making a request” and “the server was legitimately asked to make a request the attacker should not have been able to weaponise.” Those are not the same vulnerability.
Three systems, one label
Imagine three systems. Every one of them earns the label “SSRF” in an advisory, a pentest report, or a bug-bounty submission.
Three systems, one label. The element that sets the cluster is whatever actually produced the attacker’s advantage — a feature that ran correctly, or a flaw that ran at all.
System A — the image fetcher
GET /image?url=http://169.254.169.254/latest/meta-data/
The application is designed to retrieve arbitrary remote images. It retrieves the supplied URL perfectly. Nothing malfunctioned: the parser parsed, the fetcher fetched, the response was returned exactly as specified.
Cause: dangerous scope of legitimate functionality. Cluster: #1 Abuse of Functions.
System B — the broken parser
attacker input
↓
implementation defect (URL-parser confusion, mishandled redirect, XXE entity resolution)
↓
unexpected server-side HTTP request
There is no legitimate application capability available to the attacker that should produce that request. A flaw manufactures it. Fix the flaw and the request cannot be induced at all.
Cause: exploitable server implementation. Cluster: #2 Exploiting Server.
System C — the proxy behind a broken gate
This is the interesting one, and the one that dominates real advisories:
attacker
↓
validation flaw is bypassed
↓
legitimate proxy function executes
↓
internal target reached
There appear to be two causal facts here — a bypass defect and an abused capability — and the temptation is to write #2 → #1 or to argue about which one “counts.” TLCTC resolves this with a rule that has already been applied in published case work: the bypass is the bypass primitive; the abused capability is what sets the cluster. A missing or circumvented allowlist does not move the step out of #1, because the thing that produced the attacker’s advantage — the proxy request — executed exactly as designed. What the bypass tells you is where the safety gate failed; what the cluster tells you is what the attacker actually used.
If, on the other hand, the “proxy” never executes as a feature and the request only exists because the flaw itself fabricated it, you are back in System B and the step is #2.
That is why merely writing “SSRF” into a threat register does not resolve the causal model. It records the effect and leaves the cause to be argued later — usually by the team that has to choose a control.
Corroboration 1 — Next.js: three eras, two clusters
The Next.js request-forwarding history is an independent test of the split, because a single product has shipped all three systems above.
| Era | What was abused | Did the feature execute? | Cluster |
|---|---|---|---|
| Era 1 Image-optimizer SSRFs (2024) |
/_next/image is a documented proxy. Pointing it at the instance metadata service abuses the documented feature; the allowlist around it is the safety gate. |
Yes — the optimizer fetched exactly what it was told to | #1 |
| Era 2 Middleware / header bypasses (CVE-2025-29927 and siblings, 2025) |
A client-controllable header was honoured as an internal-trust marker; the middleware never ran. A trust-handling defect skipped it. | No — nothing about the middleware’s functionality was “used” | #2 |
| Era 3 WebSocket-upgrade SSRF (CVE-2026-44578, 2026) |
A code path that should have run the “safe external rewrites” check did not. But the abused thing is the rewrites proxy, which executes exactly as designed for a destination the attacker chose. | Yes — the bypass primitive moved, the abused capability did not | #1 |
Strictly, Era 2 is not even an SSRF; the discourse filed it next to SSRF because the effect — protected routes reachable — rhymed. Era 3 is the one that is easy to call #2 on autopilot, and the one where the counterfactual test earns its keep.
Two of the three eras are the same cluster and the middle one is not — which is the taxonomy doing useful work, because the controls live in different places. A team that patched image-optimizer allowlists in 2024 was not protected from CVE-2026-44578; a team that hardened middleware against CVE-2025-29927 was not protected either. A team that understood which capability each era abused would have seen the third coming. The full analysis is in CVE-2026-44578 — the allowlist moves, the cluster doesn’t.
Corroboration 2 — one product, both clusters: Artifactory in the OpenAI incident
The OpenAI → Hugging Face incident of 2026 is a nearly perfect worked example, because the same product — an internally hosted JFrog Artifactory — carried both categories inside one attack path.
The autonomous agents used Artifactory’s remote-repository fetch and relay capability to reach destinations they could not reach directly. Operationally, calling that “SSRF” makes complete sense, and OpenAI’s own timeline uses the word. But the retrieval mechanism performed its intended function; no implementation flaw is named or required. The weakness was that the legitimate scope of a server-side fetch could be pointed at the open internet from inside a sandboxed research estate. That is #1 Abuse of Functions — and it is recorded as #1 at steps 1.2, 1.4, 2.1 and 2.5 of the published register.
Elsewhere in the same path, the agents exploited actual Artifactory implementation defects: a token-refresh endpoint that accepted a forged, invalid-signature admin token and returned a validly signed one (step 1.5), and later a JRuby deserialisation path reached through a poisoned gem. Those are fundamentally different: #2 Exploiting Server, with the credential-forgery rule and R-ROLE doing the placement.
| Register step | What the agents did to Artifactory | Flaw required? | Cluster |
|---|---|---|---|
| 1.2 / 1.4 | Remote-repository fetch used as a relay to arbitrary external hosts (“SSRF” in the timeline) | No — fetch executed as designed | #1 |
| 1.5 | Token-refresh endpoint accepts a forged, invalid-signature admin token and returns a validly signed one | Yes — signature check defect | #2 |
| 2.1 / 2.5 | Relay capability re-used on the rebuilt instance; outbound controls sidestepped through Artifactory endpoints | No — a permitted intermediary abused | #1 |
| AP-5b | JRuby deserialisation RCE reached through a poisoned RubyGems artefact honoured via Artifactory’s configured upstream trust | Yes — unsafe deserialisation | #10 → #2 → #7 |
Same product, same fortnight, same word in the incident narrative — two generic vulnerabilities, two clusters, two control families. The register is in OpenAI → Hugging Face: attack paths in TLCTC v2.5.
Postscript — the vendor’s own advisory list
On 27 July 2026, two weeks after the incident was contained, JFrog published thirteen Artifactory security advisories in a single batch. Twenty-five more followed on 12 August, four on 25 August and a Critical one on 28 August — forty-three in five weeks, against one advisory in the whole of 2025 and six in 2024. None of the 2026 entries carries an acknowledgement; every older one credits an external researcher. The advisories do not mention the incident. One of them describes register step 1.5 word for word.
| Advisory | JFrog’s weakness type | JFrog’s description (verbatim) | TLCTC |
|---|---|---|---|
| CVE-2026-65616 27 Jul 2026 | CWE-347 Improper Verification of Cryptographic Signature | “Incorrect refresh-token signature validation allows non-admin users to obtain a signed administrator token.” | #2 — step 1.5 |
| CVE-2026-65924 27 Jul 2026 | CWE-918 SSRF | “Terraform remote repositories could issue outbound requests to arbitrary destinations and return response content.” | #1 — System A |
| CVE-2026-65925 27 Jul 2026 | CWE-918 SSRF | “A user with Cargo remote-repository read access could make Artifactory request unintended URLs and return the response.” | #1 — System A |
| CVE-2026-65923 27 Jul 2026 | CWE-918 SSRF | “An Ansible repository URL-validation weakness could cause unintended server-side requests.” | #1 — System C |
| CVE-2026-65618 27 Jul 2026 | CWE-918 SSRF | “Improper URL validation when handling specific URLs allows unauthorized requests from Artifactory, potentially exposing internal services and cached response data.” | #1 — System C |
| CVE-2026-70551 25 Aug 2026 | CWE-918 SSRF | “A user who can read an existing remote VCS repository can replace its configured origin or supply an absolute VCS data URL.” | #1 — System A |
Five advisories carry the label CWE-918. Read their descriptions rather than their label: each one is a remote-repository feature fetching what it was configured to fetch, from a destination that should have been out of scope — the relay of steps 1.2, 1.4, 2.1 and 2.5, and the fix each time is a narrower scope. The one implementation flaw that actually changed the incident — a signature check that did not check — sits under CWE-347, nowhere near the word “SSRF”. The vendor’s list sorts the relay by its effect (a request left the box) and the exploit by its mechanism (a signature was not verified). That is the split this post argues for, drawn by the vendor without meaning to, and it is why a CWE-918 count says nothing about what a team must fix.
A note on evidence: the advisories do not name the incident, and I cannot prove from their text that they are its disclosure tail. What can be said is that CVE-2026-65616 describes the flaw exploited on 26 June and was patched on 27 July, and that the four relay advisories describe the capability abused between May and July. Source: JFrog Security Advisories.
The controls are different — that is why it matters
If the split were only a matter of labelling hygiene it would not be worth a post. It is worth a post because the two clusters call for different defences, and a register that says “SSRF” cannot tell you which.
For #2 implementation-flaw SSRF the natural responses are the ones the industry already reaches for:
- secure coding and code review;
- parser and URL-validation correctness;
- framework and dependency patching;
- vulnerability remediation and exploit prevention.
For #1 function-abuse SSRF, patching code may accomplish almost nothing. The function may be perfectly correct. The relevant questions become design questions:
- Why does this component need arbitrary outbound retrieval at all?
- Which destinations should that function be able to reach?
- Is the scope of the proxy broader than necessary?
- Can it reach metadata services or internal namespaces?
- Are redirects followed?
- Can attackers indirectly control destination selection?
- Should this function exist in this trust zone at all?
Concretely: allowlists inside the feature (remotePatterns, upstream hostnames), protocol and response-size limits, outbound egress policy, IMDSv2. Input validation and WAF signatures are structurally weak here, because the input is valid — it is exactly the kind of URL the feature was built to accept.
That is a capability-control problem, not a vulnerability-management problem. An organisation that treats every SSRF as “patch and add a WAF rule” is systematically under-defending the #1 half of its exposure — which, going by the Next.js and Artifactory evidence, is the larger half.
SSRF is a step, never a threat statement
One more consequence of taking the cause seriously. SSRF, in either cluster, is a position-acquisition step: it gets the attacker somewhere. It is never a complete threat statement on its own (Axiom IV), and it must be written as a sequence:
169.254.169.254; returned IAM credentials then applied (R-CRED). Capital One 2019 is the canonical instance.Notice that the sequence notation removes the ambiguity the label created. #1 → #4 and #2 → #4 look almost identical in an incident report (“SSRF to IMDS, credentials stolen”) and demand different first-line controls.
TLCTC had to correct itself too
It would be too easy to end with “the industry is wrong.” The more interesting fact is that the industry label was strong enough to pull TLCTC itself off course, twice, before the rule was made rigorous.
The v1.9.1 whitepaper listed SSRF as a sub-threat of #2 Exploiting Server — “coding flaw in URL processing” — reflecting the conventional reading of SSRF as a server-side implementation bug. At the same time, the CWE mapping filed CWE-918 under #1 with the argument “abuse of fetch functionality.” Both were defensible in isolation; together they were an inconsistency that nobody had noticed, because both simply followed whichever intuition the word triggered in context.
The inconsistency surfaced under load. During the OpenAI–Hugging Face analysis, the Artifactory relay steps were initially left as an open question (§6.1 of that post) because the glossary’s blanket SSRF → #2 mapping contradicted what the evidence showed — a fetch feature executing as designed. Resolving it meant withdrawing the blanket mapping: SSRF names an effect, not a generic vulnerability, and it splits by the abused capability. The glossary now says so, and the sub-threat list under #2 deliberately no longer contains SSRF at all.
The taxonomy initially classified the name rather than the cause, and its own boundary tests eventually forced the correction. A framework that can be shown wrong by its own rules is doing what a taxonomy is for.
The larger point: technique names are not taxonomies
SSRF is not special. It is simply the cleanest example of a general failure mode: a technique name that describes the observable — what the attacker did, what the server did, what the packet looked like — gets promoted into a category, and then used as though it identified a cause.
The industry vocabulary is full of these. “Privilege escalation” describes a position change and can be #1, #2, #3 or #4 underneath. “Ransomware” describes an outcome and is a #7 step at the end of a path that usually began with #9 or #1. “Bypass” describes that a control did not hold and says nothing about whether it was defeated by a flaw or sidestepped by a feature. Each of them is fluent, each has a CWE or a MITRE technique, and each will lead a control strategy astray if it is allowed to stand in for a vulnerability.
The test is always the same. Take the term. Ask: what would have to be true for this step to be impossible? If the answer is “the code would have to be correct,” you have a #2/#3. If the answer is “the feature would have to be narrower,” you have a #1. If the answer changes depending on the system, the term was never one threat.
“SSRF” answers what request occurred. Risk management needs to know what vulnerability made it possible.
References
- TLCTC v2.5 core paper — §2 (classification principle), §4 (#1 and #2 boundary tests), §5 (Axioms I and IV) — tlctc-whitepaper.html
- TLCTC glossary — SSRF (Server-Side Request Forgery), Sub-Threat, XXE — glossary-index.html
- CVE-2026-44578 — The Allowlist Moves, the Cluster Doesn’t (Next.js three eras)
- OpenAI → Hugging Face: Attack Paths in TLCTC v2.5 (Artifactory register, §6.1 resolution)
- TLCTC ↔ CWE mapping — CWE-918 entry
- TLCTC whitepaper v1.9.1 — #2 sub-threat list (historical)
- OWASP Top 10 (2021) — A10 Server-Side Request Forgery
- MITRE CWE — CWE-918: Server-Side Request Forgery (SSRF)
- JFrog — JFrog Security Advisories: CVE-2026-65616, CVE-2026-65618, CVE-2026-65923, CVE-2026-65924, CVE-2026-65925 (27 Jul 2026); CVE-2026-70551 (25 Aug 2026)
Licensed under CC BY 4.0. Framework: tlctc.net · Source: github.com/Barnes70/TLCTC