Azure WAF Rule 931130: Why Remote File Inclusion False Positives Break Real Traffic

TL;DR — Azure WAF rule 931130 (REQUEST-931-APPLICATION-ATTACK-RFI) treats off-domain absolute URLs in request parameters as potential remote file inclusion attempts. In a modern web stack that pattern shows up everywhere: OIDC
redirect_uri, OAuthstate,return_url,Referer, and anywhere a single-page app passes a deep link around. The fast fix is to disable the rule and lose the protection. The better fix is a parameter-scoped exclusion plus an application-side allowlist of permitted hosts, documented as a runbook entry the next engineer can read in three sentences.
I have lost count of how many times I have seen Azure WAF rule 931130 block a perfectly legitimate request. The rule lives in CRS rule group 931 (Application Attack: Remote File Inclusion) and is meant to catch attackers trying to make a vulnerable application pull a file from an external URL. In a 2010 web stack that meant a query parameter like ?page=http://evil.example/shell.txt. In a 2026 web stack it means almost every OAuth callback, every share link, every “go back to where you came from” parameter in a multi-page form.
When the rule fires on a real attack, blocking is the correct outcome. When it fires on a redirect URI in an OIDC flow, the user sees a 403, the platform team sees a Slack page, and someone in the channel suggests “just disable 931130”. That suggestion is where the long version of this story starts.
The 403 that broke single sign-on
A few months ago a new internal app went live behind an Application Gateway with WAF v2 in Prevention mode. The dev team had been testing in a non-production environment without WAF in front, so the first time the request flow encountered Azure WAF was the day after go-live. Login from the browser worked. Login that bounced through Entra ID and came back to the app failed with HTTP 403.
The user pattern was:
- Browser hits
https://app.example.com/secure-page. - App sees no session cookie, builds an Entra OIDC authorization request, and redirects the browser to Entra’s authorize endpoint with
redirect_uri=https://app.example.com/auth/callbackin the query. - User authenticates at Entra.
- Entra redirects the browser back to
https://app.example.com/auth/callback?code=...&state=...&redirect_uri=https%3A%2F%2Fapp.example.com%2Fsecure-page.
Step 4 is the one that hit the WAF. The callback URL contained an encoded absolute URL in the redirect_uri query parameter. To rule 931130, that looked exactly like a remote file inclusion attempt: a query parameter whose value started with https:// followed by something the rule did not recognise as a permitted destination.
The actual error in the browser was a generic 403. The clue was the Application Gateway access log, where the request line ended in /auth/callback?code=...&redirect_uri=https%3A%2F%2Fapp.example.com%2Fsecure-page and the request reached the WAF but did not reach the backend.
What rule 931130 actually checks
Rule 931130 is the catch-all in the OWASP CRS rule group REQUEST-931-APPLICATION-ATTACK-RFI. It does not look for a specific RFI exploit; it looks for the shape of one. The match logic is roughly “is there an absolute URL whose host portion is not one of the hosts I have been told to trust, present in any argument or in the Referer header?”
The rule is conservative because remote file inclusion is one of the highest-impact OWASP categories. A successful RFI attack on a vulnerable application can give the attacker arbitrary code execution. The price the rule charges for that protection is high noise on any application that legitimately passes URLs around.
In practice, the rule fires on:
- OAuth and OIDC parameters:
redirect_uri,state,return_url,next,back,from,referrer, and any other parameter that carries a URL. - Share and embed flows where the front-end packs the source URL into the request.
- Old-school open-redirect parameters that legitimately pass URLs even when the application has a strict allowlist.
- The
Refererheader itself when it points to a non-allowed domain, which is the entire web for any user clicking through from elsewhere.
In Azure WAF prevention mode, a 931130 match contributes 5 anomaly points to the request. The default blocking threshold is also 5, so a single 931130 hit is enough to produce a 403 by itself. The rule never checks whether the URL is actually fetched by the backend. The presence of the off-domain URL in the request is the signal.
Why the modern web breaks the rule’s assumptions
The CRS rule was designed when the dominant RFI exploit pattern was a PHP include($_GET["page"]) that pulled a file from whatever URL the attacker supplied. That pattern is rare in modern stacks. Frameworks have removed the unsafe include primitives, language runtimes refuse to follow URLs the way PHP used to, and the kinds of applications still vulnerable to true RFI are mostly legacy systems that are not running behind a 2026 WAF in the first place.
What did not change is the surface area where URLs appear in HTTP requests. OAuth and OIDC are URL-heavy by design. Single-page apps pass deep-link return paths back through query strings. Webhooks and integrations let users register callback URLs. Social shares carry URLs in nearly every parameter. The rule’s assumption that URLs in query parameters are inherently suspicious has aged in the wrong direction.
The result is a high false-positive rate that the rule itself cannot distinguish from real attacks. Both produce the same shape in the request.
The KQL hunt
When I see a 931130 block on a request I cannot immediately classify, the first thing I do is pull the matched details from the firewall log. For Application Gateway WAF v2 the table is AzureDiagnostics:
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where ruleId_s == "931130"
| where TimeGenerated > ago(24h)
| project TimeGenerated, clientIp_s, requestUri_s,
details_matchVariableName_s, details_matchVariableValue_s,
hostname_s
| order by TimeGenerated desc
| take 100
The details_matchVariableName_s column tells me which parameter or header tripped the rule. The details_matchVariableValue_s column tells me what the value was. If the variable name is ARGS:redirect_uri and the value is an absolute URL on my own host, the request is almost certainly legitimate. If the variable name is ARGS:url and the value points to a host I have never heard of, it deserves a deeper look.
For Azure Front Door Premium with WAF the equivalent table is FrontDoorWebApplicationFirewallLog and the column names differ slightly, but the shape is the same: rule ID, matched variable, matched value.
A useful aggregation when triaging a noisy 931130 is to group by the matched variable to see whether the noise is concentrated on one parameter:
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where ruleId_s == "931130"
| where TimeGenerated > ago(7d)
| summarize hits = count() by details_matchVariableName_s
| order by hits desc
In most environments I have looked at, more than 80% of the 931130 noise comes from one or two parameter names: redirect_uri, return_url, next, or Referer.
The wrong fix
The fastest way out of the 403 is to disable rule 931130 entirely. In Bicep or Terraform that is one block of policy:
managed_rule_override {
rule_group_name = "REQUEST-931-APPLICATION-ATTACK-RFI"
rule {
id = "931130"
enabled = false
}
}
I have seen this commit land in production within thirty minutes of an incident. The pressure to “get login working” is real and the rule has been the obvious culprit. The cost is that the WAF no longer flags the one in ten thousand requests where 931130 would have caught an RFI attempt. That trade is rarely the right one, because the noise was concentrated on one parameter and one path, and the protection applied everywhere else.
The right fix
The Azure WAF policy model lets you scope exclusions to specific match variables. The exclusion below disables rule 931130 only when the match was on the redirect_uri argument, which keeps the rule active for every other parameter and every other header:
{
"matchVariable": "RequestArgNames",
"selectorMatchOperator": "Equals",
"selector": "redirect_uri",
"exclusionManagedRuleSets": [
{
"ruleSetType": "OWASP",
"ruleSetVersion": "3.2",
"ruleGroups": [
{
"ruleGroupName": "REQUEST-931-APPLICATION-ATTACK-RFI",
"rules": [{ "ruleId": "931130" }]
}
]
}
]
}
The same construct works for Referer, for state, and for any other parameter the application is known to use for legitimate URL passing. The pattern I follow:
- Identify the parameter or header from the WAF log.
- Confirm with the application owner that the parameter is supposed to carry URLs.
- Add a scoped exclusion for that exact parameter, rule ID, and rule group.
- Keep the rule enabled everywhere else.
For the incident I opened with, the working exclusion list ended up being four parameters: redirect_uri on /auth/callback, state on /auth/callback, return_url everywhere, and Referer everywhere. The application also added a server-side allowlist of permitted redirect hosts in the OIDC handler, which is the second half of the fix.
The application-side half of the fix
Disabling the WAF rule on a parameter removes one defence. The application has to take over the responsibility the WAF was carrying. For redirect-style parameters the protection is an allowlist of permitted destination hosts:
const ALLOWED_REDIRECT_HOSTS = new Set([
"app.example.com",
"app.example.com:8443",
]);
function isAllowedRedirect(target: string): boolean {
try {
const url = new URL(target);
return ALLOWED_REDIRECT_HOSTS.has(url.host);
} catch {
return false;
}
}
If isAllowedRedirect(redirectUri) returns false, the app rejects the request rather than blindly issuing a redirect. That closes the open-redirect class of vulnerabilities the WAF rule was indirectly defending against, even though that was not what the rule was originally designed to catch.
For URL-passing parameters that are not redirect targets, for example a parameter that records “where the user came from” for analytics, the protection is either reject-on-mismatch or treat-as-string-only. The point is that the application now owns the policy that decides what URLs are acceptable in that field.
Residual risk and how I document it
A scoped WAF exclusion is a security decision and the people running the system need to know it exists. The exclusion I just added means that if the application logic ever changes such that the parameter is interpreted as a file path or include URL, the WAF will not catch it. The application now carries that responsibility through its allowlist.
The note I write into the runbook reads something like:
Rule 931130 (REQUEST-931-APPLICATION-ATTACK-RFI) is excluded on the following parameter names:
redirect_uri,state,return_url,Referer. The exclusion is required because OIDC, OAuth, and analytics flows legitimately carry URL values in those parameters. The application enforces a host allowlist for redirect destinations inauth/oidc-callback.ts. Removing the application allowlist requires re-enabling the WAF rule or accepting an open-redirect risk.
That is the residual risk in three sentences. The next engineer who looks at the WAF policy and asks “why is this exclusion here” gets the answer without having to reverse-engineer the incident.
Where this leaves me
Every Azure WAF policy I have looked after for more than a few months has a 931130 exclusion list. The list is always smaller than the team initially thought it would need, because most of the noise concentrates on a handful of parameter names. The first time a team encounters 931130 they reach for the disable switch. The second time they ask whether the rule is doing anything useful at all.
The rule is doing something useful. It catches the residual RFI surface in legacy applications, in misconfigured proxies, and in webhook endpoints where users can register arbitrary URLs. The cost of keeping it enabled is the discipline of writing scoped exclusions and matching application-side allowlists. The cost of disabling it is a class of attack the WAF will no longer flag, and on enough production runtimes that is a class of attack worth keeping a sensor on.
The pattern I keep returning to is the same one: scope the exclusion to the parameter, document the residual risk, give the application the matching responsibility. It is more work than flipping the rule off, and it is the work that pays off the next time a different parameter starts triggering 931130 and the team can see exactly how the previous incident was handled.
Further reading
For the broader WAF tuning process across SQLi, XSS, request-size, RFI, and rule-review patterns, see the GenioCT guide to Azure WAF false positives and the Azure WAF analysis methodology piece. This article is the field-lesson companion to that broader methodology.