Most ASP.NET apps still see junk traffic that never needed a managed pipeline: double-encoded path probes, odd verbs, oversized query strings, and extension scans for .bak, .config, and leftover deploy tools. On Windows Server with IIS 10.0, Request Filtering is the cheapest place to drop that noise. It runs in the native request path, so a blocked call never reaches your app pool, middleware, or SQL connection pool.
If you only harden cookies and headers inside ASP.NET, you are already late for a large class of requests. Tighten allowVerb, fileExtensions, requestLimits, and hiddenSegments at the site or applicationHost level, then let the app focus on real work.
#What Request Filtering actually gates
The Request Filtering module evaluates the raw HTTP request before most managed modules. Useful knobs for hosted .NET sites include:
- Verbs: allow only GET, HEAD, POST, PUT, PATCH, DELETE (and OPTIONS if you truly need CORS preflight at the origin).
- URL and query limits: maxUrl, maxQueryString, and maxAllowedContentLength sized to your real API and upload needs.
- Extensions: deny known junk (.exe, .dll, .config, .cs, .pdb, .bak, .sql) unless a path truly serves them.
- Hidden segments: block bin, App_code, App_Data, App_global.asax-style segments, and common VCS folders.
- Double-escape and high-bit URL checks: reject encodings attackers use to slip past naive path rules.
Classic ASP.NET Framework apps and ASP.NET Core on the IIS in-process (or out-of-process) model both benefit. Filtering is not a substitute for authZ in the app, but it removes whole categories of scanner traffic from your logs and worker processes.
#A practical site-level baseline
Prefer site or application Web.config when you can, so the rules travel with the deploy. On shared Windows hosting, some sections may be locked at the server; if a setting is denied, ask the host to unlock requestFiltering for your site or apply the same values in the delegated config they support. Example baseline you can adapt:
<system.webServer>
<security>
<requestFiltering allowDoubleEscaping="false">
<requestLimits maxAllowedContentLength="31457280"
maxUrl="4096"
maxQueryString="2048" />
<verbs allowUnlisted="false">
<add verb="GET" allowed="true" />
<add verb="HEAD" allowed="true" />
<add verb="POST" allowed="true" />
<add verb="PUT" allowed="true" />
<add verb="PATCH" allowed="true" />
<add verb="DELETE" allowed="true" />
<add verb="OPTIONS" allowed="true" />
</verbs>
<fileExtensions allowUnlisted="true">
<add fileExtension=".config" allowed="false" />
<add fileExtension=".cs" allowed="false" />
<add fileExtension=".pdb" allowed="false" />
<add fileExtension=".exe" allowed="false" />
<add fileExtension=".dll" allowed="false" />
<add fileExtension=".bak" allowed="false" />
<add fileExtension=".sql" allowed="false" />
</fileExtensions>
<hiddenSegments>
<add segment="bin" />
<add segment="App_Data" />
<add segment="App_Code" />
<add segment=".git" />
<add segment=".vs" />
</hiddenSegments>
</requestFiltering>
</security>
</system.webServer>
Tune maxAllowedContentLength to your largest legitimate upload (value is bytes). If the site is JSON APIs only, drop OPTIONS unless a browser client needs it. If you publish a pure ASP.NET Core app with no static sensitive extensions under wwwroot, keep the deny list anyway—scanners still guess those paths.
#Verify before you ship
After deploy, confirm IIS returns 404.5 / 404.6 / 404.7 / 404.8 / 404.14-style filtering status codes (or your custom error mapping) for blocked cases, not a 500 from the app. Quick checks from a workstation:
# Verb denied when allowUnlisted=false and TRACE not listed
curl -i -X TRACE https://app.example.com/
# Hidden segment
curl -i https://app.example.com/bin/app.dll
# Denied extension probe
curl -i https://app.example.com/web.config.bak
# Oversized query (adjust length to your maxQueryString)
curl -i "https://app.example.com/api/items?q=$(python -c 'print("a"*3000)')"
Watch the IIS log sc-status and sc-substatus columns. Filtering hits should never open a SQL connection or run your startup-heavy middleware. If you use Failed Request Tracing elsewhere, you can add a rule for those substatuses during a change window, then turn it off—FREB is for diagnosis, not a permanent high-volume log.
#Hosting-floor gotchas on Windows Server 2025
IIS version branding remains IIS 10.0 on Windows Server 2025; do not hunt for an “IIS 11” feature pack. Request Filtering behavior is mature, but multi-site boxes still trip over three issues:
- Locked sections: applicationHost.config may lock verbs or fileExtensions. Site Web.config then fails to start the app—fix unlocks or server-level defaults carefully.
- Web Deploy / publish profiles: a full site publish can overwrite Web.config. Keep requestFiltering in source control and in the same transform pipeline as connection strings.
- Legacy Framework apps under nested applications: a child app can inherit parent deny lists. Explicitly allow a verb or extension only where that child needs it.
For admin changes at scale, pair config with the IIS PowerShell provider (Get/Set-WebConfigurationProperty on system.webServer/security/requestFiltering) in a controlled runbook. Prefer idempotent scripts and a staging site binding before touching production host headers.
#What not to expect from filtering alone
Request Filtering will not replace authentication, anti-forgery, output encoding, or parameterized SQL. It will not parse JSON bodies for business rules. Treat it as a coarse gate: shrink the attack and scanner surface so app-pool recycles, managed exceptions, and database contention stay tied to real users. Combine it with TLS hygiene, least-privilege app-pool identities, and the ASP.NET security headers you already ship.
Practical takeaway: add a checked-in requestFiltering baseline to every Windows-hosted ASP.NET site this week—deny unlisted verbs you do not use, cap URL and content length to measured maxima, hide bin/App_Data/.git, and deny backup and source extensions. Prove the blocks with a few curl calls and IIS substatus codes, then leave the managed pipeline for application logic instead of path probes.
Comments
No comments yet