Vulnerability Analysis

CVE-2026-42826: Azure DevOps Critical Information Disclosure — What It Is & How to Fix It

Executive Summary

CVE-2026-42826 is a perfect-score (CVSS 10.0) critical information disclosure vulnerability in Microsoft Azure DevOps, published May 7, 2026. An unauthenticated remote attacker can exploit a broken access-control path to expose sensitive project data, source code, secrets, and pipeline credentials with no user interaction required. Microsoft has already deployed a server-side fix for the cloud-hosted service, but organizations running Azure DevOps Server on-premises must act immediately to verify patch status and apply compensating controls.


1. What Is This Vulnerability?

CVE-2026-42826 is catalogued under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor. Despite the "information disclosure" label, the CVSS 3.1 impact vector scores High across all three pillars — Confidentiality (C:H), Integrity (I:H), and Availability (A:H) — which puts it in the rare category of disclosures where exposed data can be weaponized to compromise integrity and cause outages.

Technical Breakdown

The vulnerability exists in how Azure DevOps validates (or fails to validate) access tokens and authorization headers on certain API endpoints. The CVSS vector — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — tells the full story:

CVSS Attribute Value Meaning
Attack Vector N Exploitable over the network from anywhere
Attack Complexity L No special conditions or race-timing required
Privileges Required N No account or authentication needed
User Interaction N Fully automated, no victim action needed
Scope C Impact extends beyond the vulnerable component
Confidentiality H Full exposure of sensitive data
Integrity H Attacker may be able to modify data
Availability H Service disruption possible via the same path

The root cause is a failure in the authorization layer for one or more REST API endpoints within the Azure DevOps platform. An unauthenticated HTTP request, potentially using a crafted authorization header or a missing/malformed token, bypasses access controls and returns responses containing protected resources — including repository contents, pipeline variables, deployment secrets, and user metadata.

Because Azure DevOps pipelines routinely store service account tokens, PATs (Personal Access Tokens), cloud provider credentials, and signing keys as pipeline variables or secret stores, an information disclosure at this layer is effectively equivalent to a credential compromise event.

Attack Vector

A simplified attack flow looks like this:

  1. Attacker identifies a publicly reachable Azure DevOps endpoint (cloud: dev.azure.com; on-prem: any internet- or intranet-exposed ADO server).
  2. Attacker sends a crafted unauthenticated (or malformed-auth) request to a specific API endpoint — no brute-force, no phishing, no insider access needed.
  3. The API responds with a data payload that should have required at minimum Project Reader permissions.
  4. Attacker extracts secrets, PATs, pipeline variables, or source code from the response.
  5. With extracted credentials, attacker pivots to CI/CD pipelines, cloud infrastructure, or downstream systems.
# Example malformed / null-auth request pattern (conceptual — no PoC published):
GET /[org]/[project]/_apis/build/definitions?api-version=7.1 HTTP/1.1
Host: dev.azure.com
Authorization: Bearer <empty or malformed>
Accept: application/json

In a correctly configured system this returns 401 Unauthorized. With CVE-2026-42826 present, the server returns pipeline definitions and potentially embedded variable values.

Real-World Impact

No public proof-of-concept had been released as of May 9, 2026, and Microsoft reports no evidence of in-the-wild exploitation at time of disclosure. However, the combination of zero prerequisites and high data sensitivity in Azure DevOps pipelines makes this a high-priority target. Supply chain attacks — where an adversary compromises a pipeline to inject malicious code into builds or deployments — are a well-documented threat model that this vulnerability directly enables.


2. Who Is Affected?

Deployment Type Status
Azure DevOps Services (cloud) Patched automatically by Microsoft (May 7, 2026)
Azure DevOps Server 2022.x Verify patch level; update if not on latest
Azure DevOps Server 2020.x Verify patch level; update if not on latest
Azure DevOps Server 2019.x End-of-support; assume unpatched — upgrade

High-risk configurations:

  • Organizations with publicly exposed Azure DevOps Server instances (internet-facing without VPN/firewall restriction)
  • Pipelines storing secrets as plain-text pipeline variables instead of Azure Key Vault references
  • Organizations using service principals with broad permissions (Owner, Contributor) scoped to subscriptions
  • Multi-tenant development environments where external contributors have partial project access
  • Any org that has not rotated credentials since May 7, 2026

3. How to Detect It (Testing)

Manual Testing Steps

Before running any tests, ensure you have written authorization to test the target system. Do not test systems you do not own.

Step 1 — Baseline authentication test

# Test an authenticated request first (use your own valid PAT)
curl -u "":<YOUR_PAT> \
  "https://dev.azure.com/{org}/{project}/_apis/build/definitions?api-version=7.1" \
  -o auth_response.json -w "%{http_code}"
# Expected: 200 with pipeline data

Step 2 — Unauthenticated probe

# Remove auth entirely
curl -s -o unauth_response.json -w "%{http_code}" \
  "https://dev.azure.com/{org}/{project}/_apis/build/definitions?api-version=7.1"
# Patched expected: 401 or 403
# Vulnerable expected: 200 with data

Step 3 — Null token probe

# Send an empty Bearer token
curl -H "Authorization: Bearer " \
  -s -o null_token_response.json -w "%{http_code}" \
  "https://dev.azure.com/{org}/{project}/_apis/build/definitions?api-version=7.1"
# Patched expected: 401
# Vulnerable expected: 200

Step 4 — Evaluate the response

# Check if sensitive data was returned
cat unauth_response.json | python3 -m json.tool | grep -iE "secret|password|token|key|variable"

If Step 2 or Step 3 returns 200 with pipeline data, the system is vulnerable.

Automated Scanning

Nuclei (Project Discovery)

# nuclei-template: cve-2026-42826.yaml
id: CVE-2026-42826

info:
  name: Azure DevOps Unauthenticated Information Disclosure
  author: security-team
  severity: critical
  reference: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-42826

requests:
  - method: GET
    path:
      - "{{BaseURL}}/_apis/build/definitions?api-version=7.1"
      - "{{BaseURL}}/_apis/projects?api-version=7.1"
    headers:
      Authorization: "Bearer "
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - '"value"'
          - '"id"'
        condition: and
nuclei -t cve-2026-42826.yaml -u https://devops.yourorg.com -severity critical

Azure Security Center / Defender for DevOps

  • Navigate to Microsoft Defender for Cloud → DevOps Security
  • Review alerts tagged with CVE-2026-42826 or Unauthorized API Access
  • Check the Attack path analysis tab for exposure paths

Burp Suite

  1. Configure Burp as proxy for your ADO browser session
  2. Use Repeater to replay captured API requests with the Authorization header removed or blanked
  3. Observe HTTP response codes — any 200 response without auth is a finding

Code Review Checklist

For teams maintaining custom ADO Server extensions or API integrations:

  • Confirm all API calls include a valid, non-empty Authorization header
  • Verify that PATs used in pipelines are scoped to minimum required permissions
  • Check that secret pipeline variables use isSecret: true or Key Vault references — not plain text
  • Review service connections for over-permissioned credentials
  • Audit any Anonymous or AllowAnonymous access configurations in IIS (for on-prem servers)
  • Confirm network-level access controls prevent unauthenticated access from untrusted networks

4. How to Fix It (Mitigation)

Step-by-Step Remediation

For Azure DevOps Services (cloud-hosted):

  1. Verify Microsoft's patch is active — Log in to your Azure DevOps organization and confirm version in Organization Settings → Overview. The fix was deployed automatically on or after May 7, 2026.
  2. Audit and rotate exposed credentials — Assume any credentials stored in pipeline variables since before May 7, 2026 may be compromised. Rotate all PATs, service principal secrets, and API keys immediately.
  3. Review audit logs — In Organization Settings → Policies → Auditing, filter for unauthenticated or anomalous API calls from May 1–9, 2026.

For Azure DevOps Server (on-premises):

  1. Check your current version:
    # On the ADO Server machine:
    Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Team Foundation Server" | Select-Object -ExpandProperty Version
    
  2. Download and apply the latest update from the Azure DevOps Server Release Notes.
  3. Restrict network access immediately — Before patching, restrict inbound access to the ADO server via firewall or network security group:
    # Windows Firewall — block all external inbound on port 8080/443 from untrusted IPs
    New-NetFirewallRule -DisplayName "Block-ADO-External" -Direction Inbound `
      -LocalPort 443,8080 -Protocol TCP -Action Block -RemoteAddress Internet
    
  4. Restart IIS after patching:
    iisreset /restart
    
  5. Re-enable access and validate that authentication is working correctly before returning to normal operations.

Code Fix Example

If you maintain a custom application or integration that calls Azure DevOps APIs, ensure tokens are always validated on the client side before sending:

Before (vulnerable pattern):

import requests

def get_pipelines(org, project, token=""):
    url = f"https://dev.azure.com/{org}/{project}/_apis/build/definitions"
    headers = {"Authorization": f"Bearer {token}"}
    return requests.get(url, headers=headers, params={"api-version": "7.1"})

After (hardened pattern):

import requests

def get_pipelines(org: str, project: str, token: str) -> requests.Response:
    if not token or not token.strip():
        raise ValueError("A valid authentication token is required.")
    
    url = f"https://dev.azure.com/{org}/{project}/_apis/build/definitions"
    headers = {
        "Authorization": f"Bearer {token.strip()}",
        "Content-Type": "application/json",
    }
    response = requests.get(url, headers=headers, params={"api-version": "7.1"}, timeout=30)
    response.raise_for_status()
    return response

Configuration Hardening

Azure DevOps Server — IIS Authentication:

<!-- web.config — ensure Anonymous Authentication is disabled -->
<system.webServer>
  <security>
    <authentication>
      <anonymousAuthentication enabled="false" />
      <windowsAuthentication enabled="true" />
    </authentication>
  </security>
</system.webServer>

Azure Policy — Restrict public access to ADO:

{
  "mode": "All",
  "policyRule": {
    "if": {
      "allOf": [
        { "field": "type", "equals": "Microsoft.DevOps/organizations" },
        { "field": "Microsoft.DevOps/organizations/publicAccessEnabled", "equals": true }
      ]
    },
    "then": { "effect": "Deny" }
  }
}

Network Security Group (on-prem equivalent — restrict ADO Server access):

# Azure NSG rule — only allow access from known corporate IP ranges
az network nsg rule create \
  --resource-group MyRG \
  --nsg-name ADO-Server-NSG \
  --name AllowCorporateOnly \
  --priority 100 \
  --direction Inbound \
  --protocol Tcp \
  --destination-port-ranges 443 8080 \
  --source-address-prefixes "203.0.113.0/24" \  # Replace with your corporate IPs
  --access Allow

5. How to Test the Fix (Validation)

Regression Test Scenarios

  • Scenario A: Unauthenticated request to pipeline API returns 401 Unauthorized — not 200 OK.
  • Scenario B: Empty Authorization: Bearer header request is rejected with 401 — not passed through.
  • Scenario C: Authenticated request with a valid PAT still returns 200 with the correct data — no regression in normal functionality.
  • Scenario D: Audit log in ADO shows the test probe attempts as failed/unauthorized events (confirming logging is active).

Security Test Cases

Test Case 1: Verify unauthenticated access is blocked

# Precondition: Patch applied, server restarted
# Action: Send unauthenticated GET to pipeline endpoint
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
  "https://dev.azure.com/{org}/{project}/_apis/build/definitions?api-version=7.1")

if [ "$RESPONSE" == "401" ] || [ "$RESPONSE" == "403" ]; then
  echo "[PASS] Unauthenticated access correctly blocked."
else
  echo "[FAIL] Unexpected response code: $RESPONSE — possible vulnerability!"
  exit 1
fi

Test Case 2: Verify empty Bearer token is rejected

RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer " \
  "https://dev.azure.com/{org}/{project}/_apis/projects?api-version=7.1")

if [ "$RESPONSE" == "401" ] || [ "$RESPONSE" == "400" ]; then
  echo "[PASS] Empty Bearer token correctly rejected."
else
  echo "[FAIL] Empty Bearer token returned: $RESPONSE — investigate immediately!"
  exit 1
fi

Test Case 3: Verify authenticated access still works

RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
  -u "":${ADO_PAT} \
  "https://dev.azure.com/{org}/{project}/_apis/build/definitions?api-version=7.1")

if [ "$RESPONSE" == "200" ]; then
  echo "[PASS] Authenticated access works correctly."
else
  echo "[WARN] Authenticated request returned: $RESPONSE — check token validity."
fi

Automated Test Integration (CI/CD Gate)

Add the following to your security pipeline as a pre-deploy gate:

# azure-pipelines.yml — security gate step
- task: Bash@3
  displayName: "Security Gate: Verify CVE-2026-42826 Remediation"
  inputs:
    targetType: inline
    script: |
      UNAUTH=$(curl -s -o /dev/null -w "%{http_code}" \
        "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_apis/build/definitions?api-version=7.1")
      
      if [ "$UNAUTH" == "200" ]; then
        echo "##vso[task.logissue type=error]SECURITY GATE FAILED: CVE-2026-42826 — Unauthenticated access returned 200."
        exit 1
      fi
      echo "Security gate passed. Unauthenticated access returned: $UNAUTH"

6. Prevention & Hardening

Best Practices

  • Principle of least privilege for service connections: Scope all Azure DevOps service connections and service principals to the minimum required permissions. Use Reader where possible; avoid Owner-scoped credentials in pipelines.
  • Never store secrets as plain pipeline variables: Migrate all pipeline secrets to Azure Key Vault and reference them using Key Vault task integration. Plain-text variables — even isSecret: true — are still accessible via the API.
  • Network restriction by default: Azure DevOps Server instances should never be directly internet-accessible. Place them behind a VPN, WAF, or at minimum an IP allowlist. For cloud, configure IP allowlisting under Organization Settings → Security → Allowed IP addresses.
  • Enable Conditional Access for Azure DevOps: Enforce MFA and device compliance for all users via Entra ID Conditional Access policies targeting dev.azure.com.
  • Regular credential rotation: Rotate all PATs, service principal secrets, and pipeline credentials on a quarterly schedule minimum — or immediately following any security advisory.
  • Monitor for ADO version drift: Establish automated alerting when Azure DevOps Server falls behind the latest patch level. Treat unpatched ADO servers the same as unpatched web servers.

Monitoring & Detection

Key log sources to monitor:

Log Source What to Watch For
ADO Audit Log (Organization Settings → Auditing) Spikes in API calls from unknown IPs; calls returning 200 without valid auth context
Azure Monitor / Sentinel Alerts for AzureDevOpsAuditEvent with AuthenticationMechanism = Anonymous
IIS Access Logs (on-prem) 401/403 bursts from external IPs indicating scanning; 200 responses to requests with missing Authorization headers
Entra ID Sign-In Logs Service principal sign-in anomalies — new locations, high frequency

KQL query for Microsoft Sentinel:

AzureDevOpsAuditEvent
| where TimeGenerated > ago(7d)
| where AuthenticationMechanism == "Anonymous" or isempty(ActorUserId)
| where OperationName has_any ("Build.ReadDefinition", "Code.Read", "VariableGroups.Read")
| summarize Count=count(), IPs=make_set(IpAddress) by OperationName, ActorDisplayName
| where Count > 5
| order by Count desc

Set this as a Scheduled Analytics Rule with a 15-minute query frequency and alert when result count exceeds 0.


References

Latest from the blog

See all →