Skip to content

Commit 6a2f29b

Browse files
committed
Breaking: Limit X-Content-Type-Options usage
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
1 parent 437f5d8 commit 6a2f29b

File tree

4 files changed

+85
-41
lines changed

4 files changed

+85
-41
lines changed

docs/user-guide/rules/x-content-type-options.md

+30-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Require `X-Content-Type-Options` HTTP response header (`x-content-type-options`)
22

3-
`x-content-type-options` warns against not serving resources with the
4-
`X-Content-Type-Options: nosniff` HTTP response header.
3+
`x-content-type-options` warns against not serving scripts and
4+
stylesheets with the `X-Content-Type-Options: nosniff` HTTP response
5+
header.
56

67
## Why is this important?
78

@@ -25,41 +26,56 @@ hosting untrusted content.
2526
Fortunately, browsers provide a way to opt-out of MIME sniffing by
2627
using the `X-Content-Type-Options: nosniff` HTTP response header.
2728

28-
Note: [Most modern browsers only respect the header for `script`s and
29-
`style`s][fetch spec blocking] (see also [whatwg/fetch#395][fetch spec
30-
issue].
31-
3229
Going back to the previous example, if the `X-Content-Type-Options: nosniff`
3330
header is sent for the script, if the browser detects that it’s a script
3431
and it wasn’t served with one of the [JavaScript media type][javascript
3532
media types], it will block it.
3633

34+
Note: [Modern browsers only respect the header for scripts and
35+
stylesheets][fetch spec blocking], and sending the header for other
36+
resources such as images may [create problems in older browsers][fetch
37+
spec issue].
38+
3739
## What does the rule check?
3840

39-
The rule checks if responses include the `X-Content-Type-Options`
40-
HTTP headers with the value of `nosniff`.
41+
The rule checks if only scripts and stylesheets are served with the
42+
`X-Content-Type-Options` HTTP headers with the value of `nosniff`.
4143

4244
### Examples that **trigger** the rule
4345

46+
Resource that is not script or stylesheet is served with the
47+
`X-Content-Type-Options` HTTP header.
48+
4449
```text
4550
HTTP/... 200 OK
4651
4752
...
53+
54+
Content-Type: image/png
55+
X-Content-Type-Options: nosniff
4856
```
4957

58+
Script is served with the `X-Content-Type-Options` HTTP header
59+
with the invalid value of `no-sniff`.
60+
5061
```text
5162
HTTP/... 200 OK
5263
5364
...
65+
Content-Type: text/javascript; charset=utf-8
5466
X-Content-Type-Options: no-sniff
5567
```
5668

5769
### Examples that **pass** the rule
5870

71+
Script is served with the `X-Content-Type-Options` HTTP header
72+
with the valid value of `nosniff`.
73+
5974
```text
6075
HTTP/... 200 OK
6176
6277
...
78+
Content-Type: text/javascript; charset=utf-8
6379
X-Content-Type-Options: nosniff
6480
```
6581

@@ -70,13 +86,16 @@ X-Content-Type-Options: nosniff
7086
<details>
7187
<summary>How to configure Apache</summary>
7288

73-
Apache can be configured to serve resources with the
74-
`X-Content-Type-Options` header with the value of `nosniff`
89+
Presuming the script files use the `.js` or `.mjs` extension, and
90+
the stylesheets `.css`, Apache can be configured to serve the with
91+
the `X-Content-Type-Options` header with the value of `nosniff`
7592
using the [`Header` directive][header directive]:
7693

7794
```apache
7895
<IfModule mod_headers.c>
79-
Header set X-Content-Type-Options "nosniff"
96+
<FilesMatch "\.(css|m?js)$">
97+
Header set X-Content-Type-Options "nosniff"
98+
</FilesMatch>
8099
</IfModule>
81100
```
82101

src/lib/rules/x-content-type-options/x-content-type-options.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,41 @@ const rule: IRuleBuilder = {
3737
return;
3838
}
3939

40+
let headerIsRequired = false;
41+
4042
const headerValue: string = normalizeString(response.headers && response.headers['x-content-type-options']);
43+
const nodeName = element && normalizeString(element.nodeName);
44+
45+
/*
46+
* See:
47+
*
48+
* * https://github.com/whatwg/fetch/issues/395
49+
* * https://fetch.spec.whatwg.org/#x-content-type-options-header
50+
*/
51+
52+
if (nodeName === 'script' ||
53+
(nodeName === 'link' && normalizeString(element.getAttribute('rel')) === 'stylesheet')) {
54+
headerIsRequired = true;
55+
}
56+
57+
if (headerIsRequired) {
58+
if (headerValue === null) {
59+
await context.report(resource, element, `'x-content-type-options' header is not specified`);
60+
61+
return;
62+
}
63+
64+
if (headerValue !== 'nosniff') {
65+
await context.report(resource, element, `'x-content-type-options' header value (${headerValue}) is invalid`);
4166

42-
if (headerValue === null) {
43-
await context.report(resource, element, `'x-content-type-options' header was not specified`);
67+
return;
68+
}
4469

4570
return;
4671
}
4772

48-
if (headerValue !== 'nosniff') {
49-
await context.report(resource, element, `'x-content-type-options' header value (${headerValue}) is invalid`);
73+
if (headerValue) {
74+
await context.report(resource, element, `'x-content-type-options' header is not needed`);
5075
}
5176
};
5277

tests/helpers/test-server.ts

-1
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ export class Server {
281281
res.status(200);
282282
res.setHeader('Content-Length', '0');
283283
res.setHeader('Content-Type', 'image/x-icon');
284-
res.setHeader('X-Content-Type-Options', 'nosniff');
285284
res.end();
286285
});
287286
}

tests/lib/rules/x-content-type-options/tests.ts

+26-25
Original file line numberDiff line numberDiff line change
@@ -7,69 +7,70 @@ import * as ruleRunner from '../../../helpers/rule-runner';
77

88
// Error messages.
99

10-
const noHeaderMessage = `'x-content-type-options' header was not specified`;
10+
const noHeaderMessage = `'x-content-type-options' header is not specified`;
11+
const unneededHeaderMessage = `'x-content-type-options' header is not needed`;
1112
const generateInvalidValueMessage = (value: string = '') => {
1213
return `'x-content-type-options' header value (${value}) is invalid`;
1314
};
1415

1516
// Page data.
1617

17-
const generateHTMLPageData = (content: string) => {
18-
return {
19-
content,
20-
headers: { 'X-Content-Type-Options': 'nosniff' }
21-
};
22-
};
23-
24-
const htmlPageWithScriptData = generateHTMLPageData(generateHTMLPage(undefined, '<script src="test.js"></script>'));
25-
const htmlPageWithManifestData = generateHTMLPageData(generateHTMLPage('<link rel="manifest" href="test.webmanifest">'));
18+
const htmlPageWithScript = generateHTMLPage(undefined, '<script src="test.js"></script>');
19+
const htmlPageWithStylesheet = generateHTMLPage('<link rel="stylesheet" href="test.css">');
20+
const htmlPageWithManifest = generateHTMLPage('<link rel="manifest" href="test.webmanifest">');
2621

2722
// Tests.
2823

2924
const tests: Array<IRuleTest> = [
3025
{
3126
name: `HTML page is served without 'X-Content-Type-Options' header`,
32-
reports: [{ message: noHeaderMessage }],
3327
serverConfig: { '/': '' }
3428
},
3529
{
3630
name: `Manifest is served without 'X-Content-Type-Options' header`,
37-
reports: [{ message: noHeaderMessage }],
3831
serverConfig: {
39-
'/': htmlPageWithManifestData,
32+
'/': htmlPageWithManifest,
4033
'/test.webmanifest': ''
4134
}
4235
},
4336
{
44-
name: `Resource is served without 'X-Content-Type-Options' header`,
37+
name: `Script is served without 'X-Content-Type-Options' header`,
4538
reports: [{ message: noHeaderMessage }],
4639
serverConfig: {
47-
'/': htmlPageWithScriptData,
40+
'/': htmlPageWithScript,
4841
'/test.js': ''
4942
}
5043
},
44+
{
45+
name: `Stylesheet is served without 'X-Content-Type-Options' header`,
46+
reports: [{ message: noHeaderMessage }],
47+
serverConfig: {
48+
'/': htmlPageWithStylesheet,
49+
'/test.css': ''
50+
}
51+
},
5152
{
5253
name: `Resource is specified as a data URI`,
53-
serverConfig: { '/': generateHTMLPageData(generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">')) }
54+
serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') }
5455
},
5556
{
56-
name: `HTML page is served with 'X-Content-Type-Options' header with invalid value`,
57-
reports: [{ message: generateInvalidValueMessage('no-sniff') }],
58-
serverConfig: { '/': { headers: { 'X-Content-Type-Options': 'no-sniff' } } }
57+
name: `HTML page is served with the 'X-Content-Type-Options' header`,
58+
reports: [{ message: unneededHeaderMessage }],
59+
serverConfig: { '/': { headers: { 'X-Content-Type-Options': 'nosniff' } } }
5960
},
6061
{
61-
name: `Manifest is served with 'X-Content-Type-Options' header with invalid value`,
62-
reports: [{ message: generateInvalidValueMessage() }],
62+
name: `Manifest is served without 'X-Content-Type-Options' header`,
63+
reports: [{ message: unneededHeaderMessage }],
6364
serverConfig: {
64-
'/': htmlPageWithManifestData,
65-
'/test.webmanifest': { headers: { 'X-Content-Type-Options': '' } }
65+
'/': htmlPageWithManifest,
66+
'/test.webmanifest': { headers: { 'X-Content-Type-Options': 'invalid' } }
6667
}
6768
},
6869
{
69-
name: `Resource is served with 'X-Content-Type-Options' header with invalid value`,
70+
name: `Script is served with 'X-Content-Type-Options' header with invalid value`,
7071
reports: [{ message: generateInvalidValueMessage('invalid') }],
7172
serverConfig: {
72-
'/': htmlPageWithScriptData,
73+
'/': htmlPageWithScript,
7374
'/test.js': { headers: { 'X-Content-Type-Options': 'invalid' } }
7475
}
7576
}

0 commit comments

Comments
 (0)