diff --git a/packages/hint-x-content-type-options/README.md b/packages/hint-x-content-type-options/README.md index 93e0b2e590c..a6dea64fe09 100644 --- a/packages/hint-x-content-type-options/README.md +++ b/packages/hint-x-content-type-options/README.md @@ -1,7 +1,7 @@ # Use `X-Content-Type-Options` header (`x-content-type-options`) -`x-content-type-options` requires that all scripts and -stylesheets are served with the `X-Content-Type-Options: nosniff` +`x-content-type-options` requires that all resources are +served with the `X-Content-Type-Options: nosniff` HTTP response header. ## Why is this important? @@ -29,19 +29,19 @@ header is sent for the script and the browser detects that it’s a script and it wasn’t served with one of the [JavaScript media types][javascript media types], the script will be blocked. -Note: [Modern browsers only respect the header for scripts and -stylesheets][fetch spec blocking] and sending the header for other -resources (such as images) when they are served with the wrong media -type may [create problems in older browsers][fetch spec issue]. +While [modern browsers respect the header mainly for scripts and +stylesheets][fetch spec blocking], [Chromium uses this response header on +other resources][chromium ssca] for +[Cross-Origin Read Blocking][chromium corb]. ## What does the hint check? -The hint checks if all scripts and stylesheets are served with the +The hint checks if all resources are served with the `X-Content-Type-Options` HTTP headers with the value of `nosniff`. ### Examples that **trigger** the hint -Resource that is not script or stylesheet is served with the +Resource is not served with the `X-Content-Type-Options` HTTP header. ```text @@ -50,7 +50,6 @@ HTTP/... 200 OK ... Content-Type: image/png -X-Content-Type-Options: nosniff ``` Script is served with the `X-Content-Type-Options` HTTP header @@ -77,6 +76,69 @@ Content-Type: text/javascript; charset=utf-8 X-Content-Type-Options: nosniff ``` +## How to configure the server to pass this hint + +
How to configure Apache + +Apache can be configured to add headers using the [`Header` +directive][header directive]. + +```apache + + Header always set X-Content-Type-Options nosniff + +``` + +Note that: + +* The above snippet works with Apache `v2.2.0+`, but you need to have + [`mod_headers`][mod_headers] [enabled][how to enable apache modules] + for it to take effect. + +* If you have access to the [main Apache configuration file][main + apache conf file] (usually called `httpd.conf`), you should add + the logic in, for example, a [``][apache directory] + section in that file. This is usually the recommended way as + [using `.htaccess` files slows down][htaccess is slow] Apache! + + If you don't have access to the main configuration file (quite + common with hosting services), add the snippets in a `.htaccess` + file in the root of the web site/app. + +For the complete set of configurations, not just for this rule, see +the [Apache server configuration related documentation][apache config]. + +
+ +
+ +How to configure IIS + +You can add this header unconditionally to all responses. + +```xml + + + + + + + + + +``` + +Note that: + +* The above snippet works with IIS 7+. +* You should use the above snippet in the `web.config` of your + application. + +For the complete set of configurations, not just for this rule, +see the [IIS server configuration related documentation][iis config]. + +
+ ## How to use this hint? To use it you will have to install it via `npm`: @@ -115,22 +177,24 @@ And then activate it via the [`.hintrc`][hintrc] configuration file: +[chromium corb]: https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md +[chromium ssca]: https://www.chromium.org/Home/chromium-security/ssca [fetch spec blocking]: https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff%3F [fetch spec issue]: https://github.com/whatwg/fetch/issues/395 +[hintrc]: https://webhint.io/docs/user-guide/configuring-webhint/summary/ [javascript media types]: https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type [mime sniffing spec]: https://mimesniff.spec.whatwg.org/ -[hintrc]: https://webhint.io/docs/user-guide/configuring-webhint/summary/ +[apache config]: https://webhint.io/docs/user-guide/server-configurations/apache/ [apache directory]: https://httpd.apache.org/docs/current/mod/core.html#directory [header directive]: https://httpd.apache.org/docs/current/mod/mod_headers.html#header [how to enable apache modules]: https://github.com/h5bp/server-configs-apache/tree/7eb30da6a06ec4fc24daf33c75b7bd86f9ad1f68#enable-apache-httpd-modules [htaccess is slow]: https://httpd.apache.org/docs/current/howto/htaccess.html#when [main apache conf file]: https://httpd.apache.org/docs/current/configuring.html#main [mod_headers]: https://httpd.apache.org/docs/current/mod/mod_headers.html -[mod_mime]: https://httpd.apache.org/docs/current/mod/mod_mime.html -[url rewrite]: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-the-url-rewrite-module +[iis config]: https://webhint.io/docs/user-guide/server-configurations/iis/ diff --git a/packages/hint-x-content-type-options/src/hint.ts b/packages/hint-x-content-type-options/src/hint.ts index 7f95d9417d9..151647db0ed 100644 --- a/packages/hint-x-content-type-options/src/hint.ts +++ b/packages/hint-x-content-type-options/src/hint.ts @@ -10,7 +10,7 @@ */ import { debug as d } from 'hint/dist/src/lib/utils/debug'; -import { IAsyncHTMLElement, FetchEnd, IHint } from 'hint/dist/src/lib/types'; +import { FetchEnd, IHint } from 'hint/dist/src/lib/types'; import normalizeString from 'hint/dist/src/lib/utils/misc/normalize-string'; import isDataURI from 'hint/dist/src/lib/utils/network/is-data-uri'; import { HintContext } from 'hint/dist/src/lib/hint-context'; @@ -28,36 +28,6 @@ export default class XContentTypeOptionsHint implements IHint { public static readonly meta = meta; public constructor(context: HintContext) { - - const isHeaderRequired = (element: IAsyncHTMLElement | null): boolean => { - if (!element) { - return false; - } - - const nodeName = normalizeString(element.nodeName); - - /* - * See: - * - * * https://github.com/whatwg/fetch/issues/395 - * * https://fetch.spec.whatwg.org/#x-content-type-options-header - */ - - if (nodeName === 'script') { - return true; - - } - - if (nodeName === 'link') { - // We check if element exists before and `normalizeString` will return `''` as default - const relValues = (normalizeString(element.getAttribute('rel'), ''))!.split(' '); - - return relValues.includes('stylesheet'); - } - - return false; - }; - const validate = async ({ element, resource, response }: FetchEnd) => { // This check does not make sense for data URI. @@ -69,25 +39,19 @@ export default class XContentTypeOptionsHint implements IHint { const headerValue: string | null = normalizeString(response.headers && response.headers['x-content-type-options']); - if (isHeaderRequired(element)) { - if (headerValue === null) { - await context.report(resource, `Response should include 'x-content-type-options' header.`, { element }); + if (headerValue === null) { + await context.report(resource, `Response should include 'x-content-type-options' header.`, { element }); - return; - } - - if (headerValue !== 'nosniff') { - await context.report(resource, `'x-content-type-options' header value should be 'nosniff', not '${headerValue}'.`, { element }); + return; + } - return; - } + if (headerValue !== 'nosniff') { + await context.report(resource, `'x-content-type-options' header value should be 'nosniff', not '${headerValue}'.`, { element }); return; } - if (headerValue) { - await context.report(resource, `Response should not include unneeded 'x-content-type-options' header.`, { element }); - } + return; }; context.on('fetch::end::*', validate); diff --git a/packages/hint-x-content-type-options/tests/tests.ts b/packages/hint-x-content-type-options/tests/tests.ts index f391d3563fe..83bf0436385 100644 --- a/packages/hint-x-content-type-options/tests/tests.ts +++ b/packages/hint-x-content-type-options/tests/tests.ts @@ -3,19 +3,18 @@ import { getHintPath } from 'hint/dist/src/lib/utils/hint-helpers'; import { HintTest } from '@hint/utils-tests-helpers/dist/src/hint-test-type'; import * as hintRunner from '@hint/utils-tests-helpers/dist/src/hint-runner'; -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Page data. const pageWithAlternateStylesheet = generateHTMLPage(''); const pageWithScript = generateHTMLPage(undefined, ''); +const pageWithScriptAndStylesheet = generateHTMLPage('', ''); const pageWithStylesheet = generateHTMLPage(''); +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Error messages. const noHeaderErrorMessage = `Response should include 'x-content-type-options' header.`; -const unneededHeaderErrorMessage = `Response should not include unneeded 'x-content-type-options' header.`; - const generateInvalidValueMessage = (value: string = '') => { return `'x-content-type-options' header value should be 'nosniff', not '${value}'.`; }; @@ -27,21 +26,26 @@ const generateInvalidValueMessage = (value: string = '') => { const tests: HintTest[] = [ { name: `HTML page is served without 'X-Content-Type-Options' header`, - serverConfig: { '/': '' } + reports: [{ message: noHeaderErrorMessage }], + serverConfig: { + '/': { content: generateHTMLPage() }, + '/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } } + } }, { - name: `Script is served without 'X-Content-Type-Options' header`, + name: `Favicon is served without 'X-Content-Type-Options' header`, reports: [{ message: noHeaderErrorMessage }], serverConfig: { - '/': pageWithScript, - '/test.js': '' + '/': { content: generateHTMLPage(), headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } }, + '/favicon.ico': '' } }, { name: `Stylesheet is served without 'X-Content-Type-Options' header`, reports: [{ message: noHeaderErrorMessage }], serverConfig: { - '/': pageWithStylesheet, + '/': { content: pageWithStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } }, + '/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } }, '/test.css': '' } }, @@ -49,26 +53,33 @@ const tests: HintTest[] = [ name: `Alternate stylesheet is served without 'X-Content-Type-Options' header`, reports: [{ message: noHeaderErrorMessage }], serverConfig: { - '/': pageWithAlternateStylesheet, + '/': { content: pageWithAlternateStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } }, + '/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } }, '/test.css': '' } }, { name: `Resource is specified as a data URI`, - serverConfig: { '/': generateHTMLPage(undefined, '') } - }, - { - name: `HTML page is served with the 'X-Content-Type-Options' header`, - reports: [{ message: unneededHeaderErrorMessage }], - serverConfig: { '/': { headers: { 'X-Content-Type-Options': 'nosniff' } } } + serverConfig: { + '/': { + content: generateHTMLPage(undefined, ''), + headers: { 'X-Content-Type-Options': 'nosniff' } + }, + '/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } } + } }, { name: `Script is served with 'X-Content-Type-Options' header with invalid value`, reports: [{ message: generateInvalidValueMessage('invalid') }], serverConfig: { - '/': pageWithScript, + '/': { content: pageWithScript, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } }, + '/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } }, '/test.js': { headers: { 'X-Content-Type-Options': 'invalid' } } } + }, + { + name: `All resources are served with 'X-Content-Type-Options' header`, + serverConfig: { '*': { content: pageWithScriptAndStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } } } } ]; diff --git a/packages/hint/docs/user-guide/server-configurations/apache.md b/packages/hint/docs/user-guide/server-configurations/apache.md index 68fc955b230..464ef2fc265 100644 --- a/packages/hint/docs/user-guide/server-configurations/apache.md +++ b/packages/hint/docs/user-guide/server-configurations/apache.md @@ -529,6 +529,17 @@ AddDefaultCharset utf-8 # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" # +# ---------------------------------------------------------------------- +# | X-Content-Type-Options | +# ---------------------------------------------------------------------- + +# Serve resources with the x-content-type-options header set to `nosniff`. +# https://webhint.io/docs/user-guide/hints/hint-x-content-type-options/ + +# +# Header always set X-Content-Type-Options nosniff +# + # ###################################################################### # # Unnedded / Disallowed headers # diff --git a/packages/hint/docs/user-guide/server-configurations/iis.md b/packages/hint/docs/user-guide/server-configurations/iis.md index 46643221cf5..e1b3f3edc2d 100644 --- a/packages/hint/docs/user-guide/server-configurations/iis.md +++ b/packages/hint/docs/user-guide/server-configurations/iis.md @@ -47,6 +47,12 @@ related hint. + + - - - - - - - -