Skip to content

Commit

Permalink
Breaking: Limit X-Content-Type-Options usage
Browse files Browse the repository at this point in the history
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
² whatwg/fetch#395

Fix #767
Close #772
  • Loading branch information
alrra committed Jan 23, 2018
1 parent 437f5d8 commit 6a2f29b
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 41 deletions.
41 changes: 30 additions & 11 deletions 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?

Expand All @@ -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
```

Expand All @@ -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>
```

Expand Down
33 changes: 29 additions & 4 deletions src/lib/rules/x-content-type-options/x-content-type-options.ts
Expand Up @@ -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`);
}
};

Expand Down
1 change: 0 additions & 1 deletion tests/helpers/test-server.ts
Expand Up @@ -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();
});
}
Expand Down
51 changes: 26 additions & 25 deletions tests/lib/rules/x-content-type-options/tests.ts
Expand Up @@ -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' } }
}
}
Expand Down

0 comments on commit 6a2f29b

Please sign in to comment.