From 6ae8df828c582b8eb40d03342ec8faf26ef8debf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C4=83t=C4=83lin=20Mari=C8=99?= <alrraa@gmail.com> Date: Mon, 22 Jan 2018 22:26:42 -0800 Subject: [PATCH] Breaking: Limit `X-Content-Type-Options` usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change `x-content-type-options` rule so that it limits the usage of the `X-Content-Type-Options` header to scripts and stylesheets as modern browsers actually only respect the header for those types of resources¹. Also, sending the header for resources such as images, creates problems² in some older browsers. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ¹ https://fetch.spec.whatwg.org/#x-content-type-options-header ² https://github.com/whatwg/fetch/issues/395 Fix #767 --- .../rules/x-content-type-options.md | 41 +++++++++++---- .../x-content-type-options.ts | 33 ++++++++++-- tests/helpers/test-server.ts | 1 - .../lib/rules/x-content-type-options/tests.ts | 51 ++++++++++--------- 4 files changed, 85 insertions(+), 41 deletions(-) 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 <details> <summary>How to configure Apache</summary> -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 <IfModule mod_headers.c> - Header set X-Content-Type-Options "nosniff" + <FilesMatch "\.(css|m?js)$"> + Header set X-Content-Type-Options "nosniff" + </FilesMatch> </IfModule> ``` 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, '<script src="test.js"></script>')); -const htmlPageWithManifestData = generateHTMLPageData(generateHTMLPage('<link rel="manifest" href="test.webmanifest">')); +const htmlPageWithScript = generateHTMLPage(undefined, '<script src="test.js"></script>'); +const htmlPageWithStylesheet = generateHTMLPage('<link rel="stylesheet" href="test.css">'); +const htmlPageWithManifest = generateHTMLPage('<link rel="manifest" href="test.webmanifest">'); // Tests. const tests: Array<IRuleTest> = [ { 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, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">')) } + serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') } }, { - 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' } } } }