Stack guide
IIS security headers — web.config patterns and removing X-Powered-By
IIS sets the security-header baseline through `web.config`, which is good (it ships with the app, no admin console clicks) and bad (the XML syntax is unfamiliar if you don't live there day-to-day). This guide gives you the customHeaders block that handles every header Scorifya scans for, plus the URL Rewrite and runtime snippets needed to strip the IIS-default `Server` and `X-Powered-By` headers that disclose your stack.
1. The web.config customHeaders block
Add a `<httpProtocol><customHeaders>` block under `<system.webServer>` in `web.config`. Each `<add name="..." value="..." />` ships on every response. This handles HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy in one block. Avoid `<remove>` plus `<add>` patterns unless you specifically need to override an IIS default — `<add>` alone is fine for headers IIS doesn't set.
HSTS missing — nginx (server / location) (nginx)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # If TLS terminates at a CDN, set the header there instead.
HSTS missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.hsts({ maxAge: 31_536_000, includeSubDomains: true }));HSTS missing — Apache (vhost) (apache)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options missing — nginx (nginx)
add_header X-Frame-Options "DENY" always; # Use SAMEORIGIN instead of DENY only if same-origin framing is required.
X-Frame-Options missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.frameguard({ action: "deny" }));X-Frame-Options missing — Apache (apache)
Header set X-Frame-Options "DENY"
X-Content-Type-Options missing — nginx (nginx)
add_header X-Content-Type-Options "nosniff" always;
X-Content-Type-Options missing — Express (helmet) (typescript)
import helmet from "helmet"; app.use(helmet.noSniff());
X-Content-Type-Options missing — Apache (apache)
Header set X-Content-Type-Options "nosniff"
Related Scorifya checks: missing_hsts, missing_xfo, missing_xcto, missing_referrer_policy, missing_permissions_policy
2. CSP for IIS (with .NET nonce generation)
The IIS layer can serve a static CSP via the same customHeaders block, but if your app uses inline scripts you'll want per-request nonces — that's an ASP.NET (or ASP.NET Core) middleware concern, not IIS. The pattern is: middleware generates a nonce per request, stores it on `HttpContext.Items`, and overrides the `Content-Security-Policy` header with the nonce-included value. Then the Razor view renders `<script nonce="@Context.Items["cspNonce"]">`.
CSP missing — nginx (nginx)
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'" always;
CSP missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
}),
);CSP missing — Apache (apache)
Header set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'"
Weak CSP — nginx (tighten script-src) (nginx)
# Drop 'unsafe-inline' / wildcards in production; use nonces or hashes for any inline script. add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;
Weak CSP — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
},
}),
);Weak CSP — Apache (apache)
# Replace permissive policy with explicit hosts and no unsafe-inline where possible. Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"
Related Scorifya checks: missing_csp, weak_csp
3. Removing X-Powered-By and the Server version header
IIS sends two headers that disclose your stack: `X-Powered-By: ASP.NET` and `Server: Microsoft-IIS/<version>`. Strip the first by adding `<remove name="X-Powered-By" />` to your customHeaders block. The `Server` header is harder — neither IIS nor URL Rewrite can remove it directly. Use the `RemoveServerHeader` registry trick (HKLM\SYSTEM\CurrentControlSet\Services\HTTP\Parameters\DisableServerHeader = 1) or a URL Rewrite outbound rule that blanks it out.
Related Scorifya checks: x_powered_by, server_banner_version
Background
What is HSTS? HTTP Strict Transport Security explained →
How HSTS works, why the bootstrap window matters, what max-age and includeSubDomains do, and when (or whether) to submit your domain to the browser preload list.
Read more
Verify with a fresh scan
After deploy, run the scanner on the affected hostname. Headers and TLS settings update on the very next request, so you should see the score move within seconds.