Skip to content

Key Vault: ChallengeAuthPolicy request-replay fix (#47742) not ported to azure-keyvault-secrets / -certificates, and absent from all stable releases #48508

Description

@Gunther-System

Summary

The ChallengeAuthPolicy request-replay bug fixed by #47742 ("Fix Challenge Auth replay bug and update tests", merged 2026-07-08) was applied to azure-keyvault-keys and azure-keyvault-administration, but not to azure-keyvault-secrets or azure-keyvault-certificates, which vendor their own copies of _shared/challenge_auth_policy.py.

Separately, the fix is not present in any stable release yet — every current stable Key Vault package predates the merge, including azure-keyvault-keys itself.

To be clear about severity: I am not reporting this as a security vulnerability. The replayed request goes back to the vault that originally received it, and the bearer token is the shared https://vault.azure.net/.default audience that vault was already sent. verify_challenge_resource additionally blocks the cross-resource case. This is a correctness bug — an intended request is silently replaced by a duplicate of an earlier one.

Current state at HEAD

Occurrence count of _request_copy in each package's _shared/challenge_auth_policy.py:

Package _request_copy at HEAD Status
azure-keyvault-keys 1 fixed by #47742
azure-keyvault-administration 0 fixed
azure-keyvault-securitydomain 0 not affected
azure-keyvault-secrets 4 unported
azure-keyvault-certificates 4 unported

In the unfixed copies the stash is stored on the policy instance, which is created once per client rather than once per request:

  • self._request_copy: Optional[HttpRequest] = None — client-level state
  • self._request_copy = request.http_request — stores the in-flight request
  • if self._request_copy: request.http_request = self._request_copy — transplants it onto a later request

Stable releases

Package Latest stable Uploaded Contains fix?
azure-keyvault-secrets 4.11.0 2026-04-17 no
azure-keyvault-certificates 4.11.1 2026-05-05 no
azure-keyvault-keys 4.11.1 2026-05-19 no — predates the 2026-07-08 merge
azure-keyvault-administration 4.7.0 2026-05-19 no — predates the merge

The fix currently ships only in the pre-release azure-keyvault-keys 4.12.0b3.

Reproduction

This is the regression test added by #47742 (test_request_body_not_reused_across_requests), re-pointed at the unported packages. No Azure account, no network — the transport is a Mock.

pip install azure-keyvault-secrets==4.11.0 azure-keyvault-certificates==4.11.1 azure-keyvault-keys==4.12.0b3

Send a bodied POST to vault-A (elicits a challenge), then a bodiless GET to vault-B through the same client, and inspect the 4th outbound request:

=== azure-keyvault-keys 4.12.0b3 (POSITIVE CONTROL - contains #47742) ===
  4th request method : GET    (expected GET)
  4th request url    : https://vault-b.vault.azure.net/secrets/unrelated
  4th request body   : None   (expected None)
  RESULT: ok (per-request stash)

=== azure-keyvault-secrets 4.11.0 (UNPORTED) ===
  4th request method : POST   (expected GET)
  4th request url    : https://vault-a.vault.azure.net/secrets/db-password
  4th request body   : b'a duck'   (expected None)
  RESULT: prior request replayed

=== azure-keyvault-certificates 4.11.1 (UNPORTED) ===
  ... identical: POST, vault-a URL, body b'a duck'

The positive control discriminates: the fixed package passes under the identical harness, so the result is a property of the code under test rather than of the test.

Full reproduction script
import time
from unittest.mock import Mock

from azure.core.pipeline import Pipeline
from azure.core.rest import HttpRequest
from azure.core.credentials import AccessToken

CHALLENGE = Mock(
    status_code=401,
    headers={
        "WWW-Authenticate": 'Bearer authorization="https://authority.net/tenant", '
        "resource=https://vault.azure.net"
    },
)


def exercise(policy_cls, label):
    first_content = b"a duck"
    first_url = "https://vault-a.vault.azure.net/secrets/db-password"
    second_url = "https://vault-b.vault.azure.net/secrets/unrelated"
    seen = {}

    class C:
        n = 0

    def send(request):
        C.n += 1
        if C.n == 1:
            return CHALLENGE
        if C.n == 2:
            return Mock(status_code=200)
        if C.n == 3:
            return CHALLENGE
        if C.n == 4:
            seen["method"], seen["url"], seen["body"] = request.method, request.url, request.body
            return Mock(status_code=200)
        raise ValueError("unexpected request")

    cred = Mock(spec_set=["get_token"],
                get_token=Mock(return_value=AccessToken("token", time.time() + 3600)))
    pipeline = Pipeline(policies=[policy_cls(credential=cred)], transport=Mock(send=send))

    req = HttpRequest("POST", first_url)
    req.set_bytes_body(first_content)
    pipeline.run(req)
    pipeline.run(HttpRequest("GET", second_url))

    replayed = seen.get("body") == first_content or seen.get("url") == first_url
    print(f"\n=== {label} ===")
    print(f"  4th request method : {seen.get('method')}   (expected GET)")
    print(f"  4th request url    : {seen.get('url')}")
    print(f"    expected         : {second_url}")
    print(f"  4th request body   : {seen.get('body')!r}   (expected None)")
    print("  RESULT: " + ("prior request replayed" if replayed else "ok (per-request stash)"))
    return replayed


if __name__ == "__main__":
    from azure.keyvault.keys._shared.challenge_auth_policy import ChallengeAuthPolicy as KeysPolicy
    from azure.keyvault.secrets._shared.challenge_auth_policy import ChallengeAuthPolicy as SecretsPolicy
    from azure.keyvault.certificates._shared.challenge_auth_policy import ChallengeAuthPolicy as CertsPolicy

    exercise(KeysPolicy, "azure-keyvault-keys (POSITIVE CONTROL - fixed by #47742)")
    exercise(SecretsPolicy, "azure-keyvault-secrets (UNPORTED)")
    exercise(CertsPolicy, "azure-keyvault-certificates (UNPORTED)")

Impact

A call intended for one vault is emitted as a duplicate of an earlier call — the intended operation does not happen, and the earlier one is repeated. For secrets and certificates the replayed body is secret material being written a second time, so an unintended duplicate write or rotation is possible.

Suggested fix

Port #47742 to azure-keyvault-secrets and azure-keyvault-certificates (make the stash per-request rather than per-policy), and ship it in a stable release — the fix currently exists only in the pre-release azure-keyvault-keys 4.12.0b3.

Root cause of the divergence is the duplicated _shared directories: each package vendors its own copy of challenge_auth_policy.py, so a fix in one does not propagate. The .NET and Java SDKs are unaffected — they use a single shared implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ClientThis issue points to a problem in the data-plane of the library.KeyVaultcustomer-reportedIssues that are reported by GitHub users external to the Azure organization.needs-team-attentionWorkflow: This issue needs attention from Azure service team or SDK teamquestionThe issue doesn't require a change to the product in order to be resolved. Most issues start as that

    Type

    No type

    Projects

    Status
    Untriaged

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions