Lower tiers of Azure API Management can be protected from being bypassed by attacker calling the APIM instance FQDN directly with the following step
- Create base level, or per API policy that requires the FDID header, and the request to originate from AFD back-end
- TIP: Test the mitigation in an isolated policy first to test the logic, and then move this to cover the whole API management instance
* Higher tiers of APIM support Azure Application Gateway and WAF connecting directly to private IP address (this guide is applicable for APIM which don’t support private ip endpoints)
APIM References
Logic
Normally the attacker could spoof the X-Azure-FDID
header. Mitigation provides enforcing APIM policy which requires that the caller originates from AFD back-end IP range. See: in this IP range 2, that header is not user controllable, but AFD will always overwrite it, thus no attacker can satisfy both conditions ¹
¹ Originate from MS IP-range, and spoof the header at the same time
2 Note the PoC is created for IPv4, I did not research if there are IPv6 flows associated, and how APIM supports them in policies and if that originates from client settings
Creation of the policy.
I made small sample in NodeJS to automate the creation of the policy. If this was production case I would automate it via Azure Function to update the APIM to include added IP ranges from AFD to APIM when there is updates to the IP’s
Setup
(Requires node to be installed -Guide: Linux Windows)
#Initiate new Project npm init -y npm install axios --save npm install netmask --save
Create new file AFDMitigation.js
var {Netmask} = require('netmask')
var fs = require('fs')
var axios =require('axios')
var FDID = "009898a5-c6c7-4afa-beac-5f523528361a"
//update the latest list download url here
var listUrl = "https://download.microsoft.com/download/7/1/D/71D86715-5596-4529-9B13-DA13A5DE5B63/ServiceTags_Public_20210927.json"
main()
async function main () {
var {data} = await axios(listUrl).catch((error) => {
console.log('not valid URL')
})
var processed = ""
data.values.filter(item => item.name == "AzureFrontDoor.Backend" )
[0].properties.addressPrefixes.forEach((ip) => {
if (!ip.match(':')) {
var block = new Netmask(ip);
processed += `<address-range from="${block.first}" to="${block.last}" /> \n`
}
})
fs.writeFileSync('APIMPol.xml',`
<check-header name="X-Azure-FDID" failed-check-httpcode="code" failed-check-error-message="message" ignore-case="true">
<value>${FDID}</value>
</check-header>
<ip-filter action="allow">
${processed.trim()}
</ip-filter>
`
)
console.log(`file created at' ${__dirname}/APIMPol.xml`)
}
Run the policy
node AFDMitigation.js

After running the policy, copy the policy into the API you want to test the policy

Confirming results
#Will Work
$ur = "https://apimfrontdoor.azurefd.net/fn-businessvc-27556/read"
Invoke-RestMethod $ur -Headers @{"Ocp-Apim-Subscription-Key"=""}
# Wont work
$ur = "https://apimtestmi.azure-api.net/fn-businessvc-27556/read"
Invoke-RestMethod $ur -Headers @{"Ocp-Apim-Subscription-Key"=" "}
- Using the wrong headers

- Not being on the allowed IP range but correct FDID would return the following message

Example policy (Do not use it as it is, it is point in time policy, run the example with IP ranges to generate new one)
<!--
IMPORTANT:
- Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.
- To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.
- To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.
- To add a policy, place the cursor at the desired insertion point and select a policy from the sidebar.
- To remove a policy, delete the corresponding policy statement from the policy document.
- Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.
- Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.
- Policies are applied in the order of their appearance, from the top down.
- Comments within policy elements are not supported and may disappear. Place your comments between policy elements or at a higher level scope.
-->
<policies>
<inbound>
<check-header name="X-Azure-FDID" failed-check-httpcode="401" failed-check-error-message="NoFDID" ignore-case="true">
<value>009898a5-c6c7-4afa-beac-5f523528361a</value>
</check-header>
<ip-filter action="allow">
<address-range from="13.73.248.17" to="13.73.248.22" />
<address-range from="20.21.37.41" to="20.21.37.46" />
<address-range from="20.36.120.105" to="20.36.120.110" />
<address-range from="20.37.64.105" to="20.37.64.110" />
<address-range from="20.37.156.121" to="20.37.156.126" />
<address-range from="20.37.195.1" to="20.37.195.6" />
<address-range from="20.37.224.105" to="20.37.224.110" />
<address-range from="20.38.84.73" to="20.38.84.78" />
<address-range from="20.38.136.105" to="20.38.136.110" />
<address-range from="20.39.11.9" to="20.39.11.14" />
<address-range from="20.41.4.89" to="20.41.4.94" />
<address-range from="20.41.64.121" to="20.41.64.126" />
<address-range from="20.41.192.105" to="20.41.192.110" />
<address-range from="20.42.4.121" to="20.42.4.126" />
<address-range from="20.42.129.153" to="20.42.129.158" />
<address-range from="20.42.224.105" to="20.42.224.110" />
<address-range from="20.43.41.137" to="20.43.41.142" />
<address-range from="20.43.65.129" to="20.43.65.134" />
<address-range from="20.43.130.81" to="20.43.130.86" />
<address-range from="20.45.112.105" to="20.45.112.110" />
<address-range from="20.45.192.105" to="20.45.192.110" />
<address-range from="20.72.18.249" to="20.72.18.254" />
<address-range from="20.150.160.97" to="20.150.160.102" />
<address-range from="20.189.106.113" to="20.189.106.118" />
<address-range from="20.192.161.105" to="20.192.161.110" />
<address-range from="20.192.225.49" to="20.192.225.54" />
<address-range from="40.67.48.105" to="40.67.48.110" />
<address-range from="40.74.30.73" to="40.74.30.78" />
<address-range from="40.80.56.105" to="40.80.56.110" />
<address-range from="40.80.168.105" to="40.80.168.110" />
<address-range from="40.80.184.121" to="40.80.184.126" />
<address-range from="40.82.248.249" to="40.82.248.254" />
<address-range from="40.89.16.105" to="40.89.16.110" />
<address-range from="51.12.41.9" to="51.12.41.14" />
<address-range from="51.12.193.9" to="51.12.193.14" />
<address-range from="51.104.25.129" to="51.104.25.134" />
<address-range from="51.105.80.105" to="51.105.80.110" />
<address-range from="51.105.88.105" to="51.105.88.110" />
<address-range from="51.107.48.105" to="51.107.48.110" />
<address-range from="51.107.144.105" to="51.107.144.110" />
<address-range from="51.120.40.105" to="51.120.40.110" />
<address-range from="51.120.224.105" to="51.120.224.110" />
<address-range from="51.137.160.113" to="51.137.160.118" />
<address-range from="51.143.192.105" to="51.143.192.110" />
<address-range from="52.136.48.105" to="52.136.48.110" />
<address-range from="52.140.104.105" to="52.140.104.110" />
<address-range from="52.150.136.121" to="52.150.136.126" />
<address-range from="52.228.80.121" to="52.228.80.126" />
<address-range from="102.133.56.89" to="102.133.56.94" />
<address-range from="102.133.216.89" to="102.133.216.94" />
<address-range from="147.243.0.1" to="147.243.255.254" />
<address-range from="191.233.9.121" to="191.233.9.126" />
<address-range from="191.235.225.129" to="191.235.225.134" />
</ip-filter>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Testing WAF
There is excellent tool available for testing WAF by Wallarm (link)

Confirm results also from the log by this MS provided query
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK" and Category == "FrontdoorWebApplicationFirewallLog"
| extend ParsedUrl = parseurl(requestUri_s)
| summarize RequestCount = count() by Host = tostring(ParsedUrl.Host), Path = tostring(ParsedUrl.Path), RuleName = ruleName_s, Action = action_s, ResourceId
| order by RequestCount desc

Ending words
Azure Front door is cool product, and there is cost effective way to protect lower APIM tiers from Web attacks.
0 comments on “Mitigation: APIM Policy – Protect API Management from WAF Bypass with Azure Front Door”