diff --git a/docs/user-guide/rules/x-content-type-options.md b/docs/user-guide/rules/x-content-type-options.md index e0fcd8bfb8d..3031fa7b548 100644 --- a/docs/user-guide/rules/x-content-type-options.md +++ b/docs/user-guide/rules/x-content-type-options.md @@ -1,7 +1,8 @@ # Require `X-Content-Type-Options` HTTP response header (`x-content-type-options`) -`x-content-type-options` warns against not serving resources with the -`X-Content-Type-Options: nosniff` HTTP response header. +`x-content-type-options` warns against not serving scripts and +stylesheets with the `X-Content-Type-Options: nosniff` HTTP response +header. ## Why is this important? @@ -25,41 +26,56 @@ hosting untrusted content. Fortunately, browsers provide a way to opt-out of MIME sniffing by using the `X-Content-Type-Options: nosniff` HTTP response header. -Note: [Most modern browsers only respect the header for `script`s and -`style`s][fetch spec blocking] (see also [whatwg/fetch#395][fetch spec -issue]. - Going back to the previous example, if the `X-Content-Type-Options: nosniff` header is sent for the script, if the browser detects that it’s a script and it wasn’t served with one of the [JavaScript media type][javascript media types], it will block it. +Note: [Modern browsers only respect the header for scripts and +stylesheets][fetch spec blocking], and sending the header for other +resources such as images may [create problems in older browsers][fetch +spec issue]. + ## What does the rule check? -The rule checks if responses include the `X-Content-Type-Options` -HTTP headers with the value of `nosniff`. +The rule checks if only scripts and stylesheets are served with the +`X-Content-Type-Options` HTTP headers with the value of `nosniff`. ### Examples that **trigger** the rule +Resource that is not script or stylesheet is served with the +`X-Content-Type-Options` HTTP header. + ```text HTTP/... 200 OK ... + +Content-Type: image/png +X-Content-Type-Options: nosniff ``` +Script is served with the `X-Content-Type-Options` HTTP header +with the invalid value of `no-sniff`. + ```text HTTP/... 200 OK ... +Content-Type: text/javascript; charset=utf-8 X-Content-Type-Options: no-sniff ``` ### Examples that **pass** the rule +Script is served with the `X-Content-Type-Options` HTTP header +with the valid value of `nosniff`. + ```text HTTP/... 200 OK ... +Content-Type: text/javascript; charset=utf-8 X-Content-Type-Options: nosniff ``` @@ -70,13 +86,16 @@ X-Content-Type-Options: nosniff
How to configure Apache -Apache can be configured to serve resources with the -`X-Content-Type-Options` header with the value of `nosniff` +Presuming the script files use the `.js` or `.mjs` extension, and +the stylesheets `.css`, Apache can be configured to serve the with +the `X-Content-Type-Options` header with the value of `nosniff` using the [`Header` directive][header directive]: ```apache - Header set X-Content-Type-Options "nosniff" + + Header set X-Content-Type-Options "nosniff" + ``` diff --git a/src/lib/rules/x-content-type-options/x-content-type-options.ts b/src/lib/rules/x-content-type-options/x-content-type-options.ts index 1f635f278fa..9c06355acc7 100644 --- a/src/lib/rules/x-content-type-options/x-content-type-options.ts +++ b/src/lib/rules/x-content-type-options/x-content-type-options.ts @@ -37,16 +37,41 @@ const rule: IRuleBuilder = { return; } + let headerIsRequired = false; + const headerValue: string = normalizeString(response.headers && response.headers['x-content-type-options']); + const nodeName = element && normalizeString(element.nodeName); + + /* + * See: + * + * * https://github.com/whatwg/fetch/issues/395 + * * https://fetch.spec.whatwg.org/#x-content-type-options-header + */ + + if (nodeName === 'script' || + (nodeName === 'link' && normalizeString(element.getAttribute('rel')) === 'stylesheet')) { + headerIsRequired = true; + } + + if (headerIsRequired) { + if (headerValue === null) { + await context.report(resource, element, `'x-content-type-options' header is not specified`); + + return; + } + + if (headerValue !== 'nosniff') { + await context.report(resource, element, `'x-content-type-options' header value (${headerValue}) is invalid`); - if (headerValue === null) { - await context.report(resource, element, `'x-content-type-options' header was not specified`); + return; + } return; } - if (headerValue !== 'nosniff') { - await context.report(resource, element, `'x-content-type-options' header value (${headerValue}) is invalid`); + if (headerValue) { + await context.report(resource, element, `'x-content-type-options' header is not needed`); } }; diff --git a/tests/helpers/test-server.ts b/tests/helpers/test-server.ts index d57ffad6563..0a570293629 100644 --- a/tests/helpers/test-server.ts +++ b/tests/helpers/test-server.ts @@ -281,7 +281,6 @@ export class Server { res.status(200); res.setHeader('Content-Length', '0'); res.setHeader('Content-Type', 'image/x-icon'); - res.setHeader('X-Content-Type-Options', 'nosniff'); res.end(); }); } diff --git a/tests/lib/rules/x-content-type-options/tests.ts b/tests/lib/rules/x-content-type-options/tests.ts index cd97dd31beb..747bd7224a9 100644 --- a/tests/lib/rules/x-content-type-options/tests.ts +++ b/tests/lib/rules/x-content-type-options/tests.ts @@ -7,69 +7,70 @@ import * as ruleRunner from '../../../helpers/rule-runner'; // Error messages. -const noHeaderMessage = `'x-content-type-options' header was not specified`; +const noHeaderMessage = `'x-content-type-options' header is not specified`; +const unneededHeaderMessage = `'x-content-type-options' header is not needed`; const generateInvalidValueMessage = (value: string = '') => { return `'x-content-type-options' header value (${value}) is invalid`; }; // Page data. -const generateHTMLPageData = (content: string) => { - return { - content, - headers: { 'X-Content-Type-Options': 'nosniff' } - }; -}; - -const htmlPageWithScriptData = generateHTMLPageData(generateHTMLPage(undefined, '')); -const htmlPageWithManifestData = generateHTMLPageData(generateHTMLPage('')); +const htmlPageWithScript = generateHTMLPage(undefined, ''); +const htmlPageWithStylesheet = generateHTMLPage(''); +const htmlPageWithManifest = generateHTMLPage(''); // Tests. const tests: Array = [ { name: `HTML page is served without 'X-Content-Type-Options' header`, - reports: [{ message: noHeaderMessage }], serverConfig: { '/': '' } }, { name: `Manifest is served without 'X-Content-Type-Options' header`, - reports: [{ message: noHeaderMessage }], serverConfig: { - '/': htmlPageWithManifestData, + '/': htmlPageWithManifest, '/test.webmanifest': '' } }, { - name: `Resource is served without 'X-Content-Type-Options' header`, + name: `Script is served without 'X-Content-Type-Options' header`, reports: [{ message: noHeaderMessage }], serverConfig: { - '/': htmlPageWithScriptData, + '/': htmlPageWithScript, '/test.js': '' } }, + { + name: `Stylesheet is served without 'X-Content-Type-Options' header`, + reports: [{ message: noHeaderMessage }], + serverConfig: { + '/': htmlPageWithStylesheet, + '/test.css': '' + } + }, { name: `Resource is specified as a data URI`, - serverConfig: { '/': generateHTMLPageData(generateHTMLPage(undefined, '')) } + serverConfig: { '/': generateHTMLPage(undefined, '') } }, { - name: `HTML page is served with 'X-Content-Type-Options' header with invalid value`, - reports: [{ message: generateInvalidValueMessage('no-sniff') }], - serverConfig: { '/': { headers: { 'X-Content-Type-Options': 'no-sniff' } } } + name: `HTML page is served with the 'X-Content-Type-Options' header`, + reports: [{ message: unneededHeaderMessage }], + serverConfig: { '/': { headers: { 'X-Content-Type-Options': 'nosniff' } } } }, { - name: `Manifest is served with 'X-Content-Type-Options' header with invalid value`, - reports: [{ message: generateInvalidValueMessage() }], + name: `Manifest is served without 'X-Content-Type-Options' header`, + reports: [{ message: unneededHeaderMessage }], serverConfig: { - '/': htmlPageWithManifestData, - '/test.webmanifest': { headers: { 'X-Content-Type-Options': '' } } + '/': htmlPageWithManifest, + '/test.webmanifest': { headers: { 'X-Content-Type-Options': 'invalid' } } } }, { - name: `Resource is served with 'X-Content-Type-Options' header with invalid value`, + name: `Script is served with 'X-Content-Type-Options' header with invalid value`, reports: [{ message: generateInvalidValueMessage('invalid') }], serverConfig: { - '/': htmlPageWithScriptData, + '/': htmlPageWithScript, '/test.js': { headers: { 'X-Content-Type-Options': 'invalid' } } } }