Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: Limit X-Content-Type-Options usage to scripts and stylesheets #772

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions docs/user-guide/rules/x-content-type-options.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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