The response has a body but no Content-Type
What this check looks for
The server sent content without saying what kind of content it is, so each browser guesses. Guessing wrong renders the page as raw text — or runs a file as code.
Why it matters
This is a rendering bug and a security hole at the same time. Content sniffing is precisely the behaviour X-Content-Type-Options exists to switch off, and with no Content-Type at all there is nothing for that header to protect.
When the check passes, your report says: “The response declares the kind of content it carries”.
What it costs your score
When this check fails it removes 10 points from your HTTP score, before the status, confidence and repeat multipliers are applied. HTTP carries a weight of 7 in the overall score.
It shares the http.content-type family ceiling of 12 points: however many findings that family produces, together they cannot remove more than that from HTTP. One underlying problem showing up in several places is still one problem.
- Severity
- medium
- Default confidence
- confirmed
- Status when triggered
- fail
- Deduction
- 10 points
- Family cap
- http.content-type · 12
- Category
- HTTP
- Module
- Http headers
- Fix owned by
- user
- In the ruleset since
- 2026.09
How to fix it
Declare a Content-Type on every response that has a body.
Without one the browser guesses the type, which breaks rendering and re-opens the sniffing attack surface.
Set it where the response is built. An application that writes the body directly must set the header directly; a web server serving files from disk derives it from the extension.
Include the charset for any text type:
text/html; charset=utf-8.Use the accurate type rather than a catch-all —
application/json,image/svg+xml,application/pdf.application/octet-streamis correct only for a genuine download.For anything user-uploaded, serve it with a type you determined server-side, never one the uploader supplied.
Add
X-Content-Type-Options: nosniffonce the types are correct, so browsers stop second-guessing them.
How to confirm it worked
curl -sSI https://‹host›/ | grep -i '^content-type:' — expect a type, with a charset for text
Content-Type: text/html; charset=utf-8
X-Content-Type-Options: nosniffA named slot like ‹domain› — and the braces left in the configuration below — is filled in with your own values when this rule appears on a report.
Remediation by platform
# Adds charset=utf-8 to the types listed in charset_types (text/html is always included):
charset utf-8;
# Only affects files nginx serves itself, never a proxied response:
include mime.types;
default_type application/octet-stream;AddDefaultCharset utf-8
<IfModule mod_headers.c>
Header always set Content-Type "text/html; charset=utf-8" "expr=%{CONTENT_TYPE} =~ m#^text/html$#"
</IfModule>header Content-Type "text/html; charset=utf-8"<meta charset="utf-8">For a response from an application behind
proxy_pass, nginx forwards the upstream's Content-Type untouched — the fix belongs in the application.default_typeapplies only to files nginx serves whose extension it cannot map.Shown because the response identified Apache. Put it in the virtual host or server configuration rather than .htaccess: .htaccess is re-read on every request, is often disabled by AllowOverride None, and runs too late to influence a response the virtual host generates itself.
AddDefaultCharsetappends the charset to text/plain and text/html responses that do not already carry one, and is the smaller of the two changes; the Header rule is only needed if something later in the chain overwrites it.Caddy's static file server already appends
charset=utf-8to text types it detects, so this is needed mainly in front of a reverse-proxied application. Scope it with a matcher so it cannot mislabel images or downloads.A fallback, not the fix: the HTTP header wins over the meta tag whenever both are present, and the tag has to appear within the first 1024 bytes of the document to be honoured at all.
Technical detail
The response from ‹origin› had a body and no Content-Type header. ‹detail›
RFC 9110 §8.3 says a sender that generates a message containing content **should** generate a Content-Type, and that a recipient receiving content without one may inspect the content to determine its type — which is the specification describing sniffing rather than endorsing it. The concrete consequences:
- The browser's guess is not a standard. Different engines reach different conclusions about the same bytes, so the same URL can render as a page in one browser and download as a file in another.
- Sniffing is the attack surface. A file uploaded as an image but containing HTML can be sniffed as HTML and executed as a page on your own origin.
X-Content-Type-Options: nosniffcloses that — by making the browser trust the declared type absolutely, which requires there to *be* a declared type. - Character encoding is unknowable, so a document's text is decoded by heuristic as well.
A missing Content-Type almost always comes from an application writing the body directly rather than from the web server, which sets one from the file extension. Detected server: ‹detected server›.
Standards and references
Test this on your domain
Run the check that produces this finding, on its own, against any domain.
Other http headers checks
- The caching instructions contradict each other
- A text response does not declare its character encoding
- The declared Content-Encoding may not match the body
- A compressible response was sent uncompressed
- The response sends obsolete headers, one of them harmful
- No Alt-Svc header advertises HTTP/3
- A response header leaks an internal address
- The response does not say how long it may be cached
- The response cannot be revalidated without downloading it again
- A compressed response does not vary on Accept-Encoding
- Vary: * makes the response uncacheable by any shared cache