Azure's cross-tenant hole got four fixes. The fourth fix reopened the third hole.
Azure's cross-tenant hole got four fixes. The fourth fix reopened the third hole. Binary Security's "Azure's Weakest Link" series just landed its third installment (published 2026-10-02, on Hacker News' front page toda
Azure's cross-tenant hole got four fixes. The fourth fix reopened the third hole.
Binary Security's "Azure's Weakest Link" series just landed its third installment (published 2026-10-02, on Hacker News' front page today with 255 points), and it reads less like a vulnerability report than a tragicomedy in five acts. One researcher, one Azure subsystem โ API Connections, the part of Logic Apps that lets workflows talk to backends โ five critical elevation-of-privilege vulnerabilities, and $200,000 in bounties. That is $40,000 per critical on average, which is either a generous program or a very leaky abstraction. The post suggests it is both.
The architecture that makes it possible
The design is the vulnerability, and the author says so plainly. A Logic App queries a shared Azure API Management instance, which checks the connector's swagger definition, then swaps the caller's token for the token configured on the backend connection, then finishes the call against the backend. The backend can be anything: a Key Vault, an Azure SQL database, Jira, Salesforce.
The catch is the middle step. The APIM instance that performs the token swap is reached through ARM, and ARM, as a resource manager, effectively has access to everything. So if you can trick the calling service into operating on a different connection than your own, you spend the victim's token on the victim's backend. The only thing standing in the way is input validation bolted onto various layers โ and, as the post demonstrates five times, bolted-on validation is exactly the kind that misses edge cases.
The five acts
| # | Primitive | What it got the attacker |
|---|---|---|
| 1 |
DynamicInvoke path traversal (part 2) |
Cross-tenant Key Vault secret read. Four ../ segments, percent-encoded as %2e%2e, walked from the attacker's connection to the victim's: path/%2e%2e/.../apim/keyvault/<victim-id>/secrets/victimsecret/value. Response: HTTP 200, secret value dontreadme. |
| 2 | Undocumented sibling endpoints | After Microsoft restricted DynamicInvoke's paths, the researcher found four more functions sitting next to it in the extracted Logic App host code โ DynamicList and friends, reachable by swapping the name in the URI. Same traversal, now a cross-tenant SQL write: Insert INTO MySecrets VALUES ('NewEvilSecrets', 3) against the victim's database. |
| 3 |
host.api.RuntimeUrl swap |
Consumption Logic Apps create Version 1 connections, and unlike dedicated-host Standard apps, they have no managed identity to authorize โ so every Consumption app implicitly reaches every V1 connection, gated only by validation. The host.api.RuntimeUrl parameter let a workflow keep the validated victim connection reference but execute against a completely different runtime URL, as long as it ended in azure-apihub.net. The victim's secret came back. |
| 4 | RuntimeUrl path traversal | Microsoft fixed #3 with an exact-match check: the runtime URL must equal the expected endpoint for the referenced connection. Sane โ except the check ran before path normalization. A URI that matched the expected prefix string normalized to somewhere else. Same result, again. |
| 5 | The regression | When Microsoft fixed #4's traversal, the original RuntimeUrl path was no longer validated at all. Attack #3 worked again, with the same screenshots. The fix for the bypass re-opened the original hole. |
Four fixes shipped. One of them undid an earlier one. The final state, per the author, is that the dynamic endpoints are simply blocked and no other mitigations seem to be in place โ "if these endpoints ever become accessible again, I would assume they would be vulnerable."
I rebuilt the mechanism, not the exploit
I have no Azure account in this story and I did not touch a live tenant. But the core mechanism โ validate the raw value, resolve the normalized one โ is twenty lines of Python, and it is worth seeing because it is the same bug wearing two costumes:
from urllib.parse import unquote, urlparse
import posixpath
def naive_validator_ok(raw_path: str) -> bool:
return "../" not in raw_path # the slap-dash fix
def resolve(base_path: str, raw_path: str) -> str:
return posixpath.normpath(posixpath.join(base_path, unquote(raw_path)))
case1 passes raw validator: True
case1 traversal segments: 4
case1 resolved: /apim/keyvault/fd8d0f4f4069495991ccb4974f96a1ed/secrets/victimsecret/value
case1 escapes own connection: True
case2 passes prefix validator: True
case2 normalized path: /apim/apim/sql/88d28f5ff7b642cfb91df184938d074c
case2 still under expected endpoint: False
control passes validator: True
control stays inside: True
bounty per critical on average: $40,000
The validator sees %2e%2e and shrugs โ there is no literal ../ in the string. The resolver decodes first, then normalizes, and walks out of the connection. Same shape in case 2: a prefix check on the raw URL passes, normalization moves it. The control case stays inside, which is the point: the check is not wrong on honest input. It is wrong exactly on the input an attacker sends, which is the only input a security check exists for.
The lesson that survives all five acts
Every one of these bugs is a check running at the wrong layer of abstraction: on the raw string instead of the decoded one, on the pre-normalization URI instead of the resolved one, on the referenced connection instead of the executed runtime. And the series' real punchline is organizational: the researcher deliberately reported DynamicList alone first, hoping Microsoft would fix the whole family โ and got a blocklist. He reported the RuntimeUrl traversal and got a fix that regressed the previous fix.
"Validate what you resolve, not what you receive" is the kind of sentence that sounds obvious until you count how many fixes it took to not implement it. Four. And the fifth vulnerability was the fourth fix.
Sources: Binary Security, "Azure's Weakest Link โ Five Full Cross-Tenant Compromises" (2026-10-02), including the DynamicInvoke and DynamicList payloads and the $200,000 total; my own check-vs-resolve model above (2026-10-30). All five vulnerabilities are reported fixed; the mechanism demo uses no live Azure service.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.