Microsoft .NET Custom Errors Not Set means an IIS server returns ASP.NET’s detailed error page, with stack traces, file paths and version numbers, to remote visitors. Fix it by setting customErrors mode to RemoteOnly or On in web.config, and httpErrors errorMode to DetailedLocalOnly or Custom, then rescan from a remote machine.
What the scanner is actually detecting
Nessus reports this through two remote plugins in the Web Servers family. They test two layers of the same Windows web stack, so they are fixed together but verified separately.
| Plugin ID | Finding title | Layer it tests | Tenable rating |
|---|---|---|---|
| 24244 | Microsoft .NET Custom Errors Not Set | ASP.NET runtime (system.web/customErrors) |
Medium, CVSS v2 4.3 |
| 58363 | IIS Detailed Error Information Disclosure | IIS HTTP error module (system.webServer/httpErrors) |
Medium, CVSS v2 5.0, CVSS v3 5.3 |
Plugin 24244 fires when ASP.NET sends its verbose error page (the “yellow screen of death”) to the scanner. Tenable’s solution text is simply to set customErrors mode to On instead of Off. Plugin 58363 fires when IIS itself returns a detailed error page instead of a custom one. Both judge the server by what comes back over HTTP, not by reading your configuration files.
The two layers are independent. Microsoft’s IIS documentation explains that IIS generally leaves an error body already written by a module such as ASP.NET in place (the exact behavior depends on the existingResponse setting). So tightening httpErrors alone may not hide an ASP.NET stack trace, and fixing customErrors alone will not hide IIS detailed errors for requests that never reach ASP.NET, such as a missing static file. This is a runtime configuration finding of the kind dynamic testing catches, as covered in our explainer on how SAST, DAST and SCA differ: the code can be fine while the deployed configuration leaks.
Real-world risk
This is information disclosure, not a remote code execution bug. On its own it does not give anyone access. What it gives is reconnaissance:
- Physical file system paths of the site, which both error pages can show.
- Exact .NET Framework and ASP.NET version strings in the footer of the ASP.NET error page, useful for matching known vulnerabilities.
- Exception messages and stack traces, and when compilation debug is enabled, snippets of source code. Exceptions thrown by data access code can expose table names, query structure or internal host names.
- On IIS detailed errors, the module and handler that failed, plus the logon method and logon user for the request.
The practical danger is that verbose errors make other attacks cheaper: someone probing for SQL injection or path traversal gets precise feedback on every failed attempt. Tenable’s Medium rating is fair. Treat it as a quick configuration fix, not an emergency.
How to confirm it on the host
From a remote machine
Test from a machine other than the web server, because the default settings of both layers still show details to local requests. Request two pages that do not exist: an .aspx page, which ASP.NET handles, and a static file, which IIS handles. On Windows PowerShell 5.1 call curl.exe explicitly, since curl is an alias for Invoke-WebRequest there.
curl.exe -sk -o probe-aspx.html -w "%{http_code}n" https://app.example.com/no-such-page-7f3a.aspx
curl.exe -sk -o probe-static.html -w "%{http_code}n" https://app.example.com/no-such-file-7f3a.txt
findstr /i /l /c:"Version Information" /c:"Stack Trace" /c:"Exception Details" /c:"Physical Path" /c:"Logon User" probe-aspx.html probe-static.html
From Linux or macOS:
curl -sk https://app.example.com/no-such-page-7f3a.aspx | grep -Ei "Version Information|Stack Trace|Exception Details"
curl -sk https://app.example.com/no-such-file-7f3a.txt | grep -Ei "Detailed Error Information|Physical Path|Logon User"
Any match means a remote client is getting a detailed page. A generic “Runtime Error” page that tells you to change the customErrors setting, or IIS’s short “file or directory not found” message, means that layer is already hiding details.
On the server
From an elevated Command Prompt, find every configuration location that switches protection off. AppCmd searches the whole IIS configuration hierarchy, including nested applications and virtual directories:
%windir%system32inetsrvappcmd.exe search config /section:system.web/customErrors /mode:Off
%windir%system32inetsrvappcmd.exe search config /section:system.webServer/httpErrors /errorMode:Detailed
To see the effective value for one site or application, including inherited defaults:
%windir%system32inetsrvappcmd.exe list config "Default Web Site/" /section:system.web/customErrors /config:*
%windir%system32inetsrvappcmd.exe list config "Default Web Site/" /section:system.webServer/httpErrors /config:*
The same check for every site root, using the WebAdministration PowerShell module:
Import-Module WebAdministration
Get-Website | ForEach-Object {
$p = "IIS:Sites$($_.Name)"
[pscustomobject]@{
Site = $_.Name
customErrors = Get-WebConfigurationProperty -PSPath $p -Filter 'system.web/customErrors' -Name mode
httpErrors = Get-WebConfigurationProperty -PSPath $p -Filter 'system.webServer/httpErrors' -Name errorMode
}
}
customErrors can also be set in machine.config or the root web.config, so check those too (repeat with the 32-bit Framework folder if any application pool runs 32-bit):
findstr /i /n /l /c:"<customErrors" /c:"<deployment" "%windir%Microsoft.NETFramework64v4.0.30319Configmachine.config" "%windir%Microsoft.NETFramework64v4.0.30319Configweb.config"
How to fix it
1. ASP.NET: set customErrors in web.config
In each application’s web.config, under system.web:
<configuration>
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="~/Error.aspx" />
</system.web>
</configuration>
RemoteOnly is Microsoft’s documented default: remote clients get the custom page, while requests from the server itself still see full details. mode="On" hides details from everyone, including the local console. defaultRedirect is optional; without it, users see a generic error page. A static page such as Error.html is the safer target, because an .aspx error page can fail in its own right.
Choose mode="On" if a reverse proxy or load balancer component runs on the same server. ASP.NET treats a request as local when it comes from 127.0.0.1 or from the server’s own IP address (see HttpRequest.IsLocal), so behind a same-host proxy every visitor looks local and RemoteOnly shows them the full error. For the same reason, use Custom rather than DetailedLocalOnly for IIS in that setup.
Command-line equivalents, which write to the site’s web.config:
%windir%system32inetsrvappcmd.exe set config "Default Web Site/" /section:system.web/customErrors /mode:RemoteOnly
Set-WebConfigurationProperty -PSPath 'IIS:SitesDefault Web Site' -Filter 'system.web/customErrors' -Name mode -Value RemoteOnly
Repeat for every nested application the search command listed, for example "Default Web Site/legacyapp".
2. Optional: enforce it machine-wide with retail mode
To stop individual web.config files from switching detailed errors back on, add this to machine.config under system.web:
<system.web>
<deployment retail="true" />
</system.web>
According to Microsoft’s deployment element documentation, retail mode is set only at machine level. It disables trace output, debugging and detailed remote errors, forces customErrors mode to On, and overrides application web.config files. Edit machine.config in both the Framework and Framework64 v4.0.30319Config folders, and still keep debug="false" in application web.config files, as Microsoft advises.
3. IIS: set httpErrors errorMode
Server-wide, written to applicationHost.config:
%windir%system32inetsrvappcmd.exe set config /section:system.webServer/httpErrors /errorMode:DetailedLocalOnly /commit:apphost
Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.webServer/httpErrors' -Name errorMode -Value DetailedLocalOnly
Or per site, in web.config under system.webServer:
<system.webServer>
<httpErrors errorMode="DetailedLocalOnly" />
</system.webServer>
DetailedLocalOnly is the IIS default. Custom turns detailed errors off even for local requests, and Detailed is the value that causes plugin 58363. A server-level value does not win over a site or application web.config that explicitly sets errorMode="Detailed", so fix every location the search command returned. In IIS Manager the same setting is under Error Pages, then Edit Feature Settings in the Actions pane.
How to verify the fix and rescan
- Saving web.config makes ASP.NET restart the application, and IIS picks up applicationHost.config changes on its own. For a clean start anyway, recycle the pool:
Restart-WebAppPool -Name "DefaultAppPool". - Rerun both curl probes from a remote machine. Neither should match any marker.
- Rerun the two
appcmd search configcommands. They should return nothing. - Rescan the same host and port with Nessus. A narrow Advanced Scan policy with plugins 24244 and 58363 enabled is enough for confirmation; both list
http_version.naslas a dependency, so keep web server detection enabled. Scan every port and virtual host the original finding listed.
What can break and how to roll back
- Browser-based debugging stops. Developers who read stack traces in the browser will see the generic page. Point them at logs instead: ASP.NET health monitoring writes unhandled exceptions to the Windows Application event log by default, and IIS Failed Request Tracing covers IIS-level failures.
- Local troubleshooting gets harder with
mode="On"orerrorMode="Custom", since details disappear on the console as well. - Redirect side effects. With
defaultRedirectand the defaultredirectMode(ResponseRedirect), clients are redirected to the error page instead of receiving the original error status, which API clients and health checks may misread.redirectMode="ResponseRewrite"keeps the original URL; test before rollout. - Configuration errors. Malformed XML in web.config takes the application down with an HTTP 500.19 error. If the httpErrors section is locked at server level, a site web.config that sets it also fails; set it at server level instead.
- Retail mode is global. It affects every ASP.NET application on the server, including staging sites on shared machines.
To roll back IIS server settings, take a backup before you start with appcmd add backup BeforeCustomErrors and restore it with appcmd restore backup BeforeCustomErrors. Restoring stops the server and returns global configuration to the backed-up state, so plan a short window. For web.config and machine.config, copy the file aside before editing and copy it back to revert.
Common false positive reasons
Genuine false positives are uncommon, because both plugins report a response the server really returned. Check these before disputing one:
- The scan ran locally. A scanner on the web server itself, or on an address the server considers its own, receives detailed pages by design. Rescan from a remote scanner. If real users also arrive through a proxy on the same server, the finding is valid.
- A different application answered. Scanners often connect by IP, which reaches whichever site holds the catch-all binding, not the host-header site you fixed. Nested applications and vendor products with their own web.config can also keep
mode="Off". Reproduce against the exact URL or virtual host in the plugin output. - Stale results. The finding came from a scan before the change. Rescan before closing or disputing it.
- An upstream device hides it externally. A WAF or load balancer may replace error pages for internet users while the scanner reached the server directly. The direct path still leaks, so fix the server rather than excluding the finding.
FAQ
Is RemoteOnly enough, or do I need mode=”On”?
RemoteOnly clears plugin 24244 when the scan comes from another machine, and it is Microsoft’s default. Use On when a reverse proxy runs on the same host, or when details should not appear even on the console.
Does customErrors apply to ASP.NET Core?
No. ASP.NET Core does not use customErrors. Its detailed page is the Developer Exception Page, which current templates enable only in the Development environment. Make sure ASPNETCORE_ENVIRONMENT is not set to Development on production servers, and use UseExceptionHandler for production error handling.
I fixed web.config but Nessus still reports it. Why?
Usually the other layer is still open, another site or nested application on the same port has its own override, or the scan counted as local. Rerun the appcmd searches and remote probes.
How serious is this finding?
Tenable rates both plugins Medium (CVSS v2 4.3 for 24244, CVSS v3 5.3 for 58363). It is information disclosure that helps attackers refine other attacks; it does not grant access by itself.
Tracking this finding across many hosts
On a large IIS estate the hard part is proving every site, nested application and port is closed, not editing one web.config. SITEY is one option: it imports findings from 16 scanners, including Nessus results uploaded as .nessus exports, and its AI drafts host-specific remediation scripts that SITEY agents on Windows and Linux endpoints run only after human approval. Per-finding retest for Nessus, Acunetix and Burp findings then confirms the fix before closure.
Sources
- Tenable: Microsoft .NET Custom Errors Not Set (Nessus plugin 24244)
- Tenable: IIS Detailed Error Information Disclosure (Nessus plugin 58363)
- Microsoft Learn: HTTP Errors <httpErrors>
- Microsoft Learn: How to Use HTTP Detailed Errors in IIS
- Microsoft Learn: customErrors Element (ASP.NET Settings Schema)