Skip to content

Commit 806740b

Browse files
utsavizedmolant
authored andcommitted
Breaking: Update hint to check for XCTO header on all resources
This reverts changes added to check for this header only on scripts and stylesheets, and instead, checks for the header on all resources. MDN suggests the former, but Chromium uses this response header on more than script/stylesheets for CORB. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Fix #1221 Close #1842
1 parent b230947 commit 806740b

File tree

3 files changed

+112
-73
lines changed

3 files changed

+112
-73
lines changed

packages/hint-x-content-type-options/README.md

+76-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Use `X-Content-Type-Options` header (`x-content-type-options`)
22

3-
`x-content-type-options` requires that all scripts and
4-
stylesheets are served with the `X-Content-Type-Options: nosniff`
3+
`x-content-type-options` requires that all resources are
4+
served with the `X-Content-Type-Options: nosniff`
55
HTTP response header.
66

77
## Why is this important?
@@ -29,19 +29,19 @@ header is sent for the script and the browser detects that it’s a script
2929
and it wasn’t served with one of the [JavaScript media types][javascript
3030
media types], the script will be blocked.
3131

32-
Note: [Modern browsers only respect the header for scripts and
33-
stylesheets][fetch spec blocking] and sending the header for other
34-
resources (such as images) when they are served with the wrong media
35-
type may [create problems in older browsers][fetch spec issue].
32+
While [modern browsers respect the header mainly for scripts and
33+
stylesheets][fetch spec blocking], [Chromium uses this response header on
34+
other resources][chromium ssca] for
35+
[Cross-Origin Read Blocking][chromium corb].
3636

3737
## What does the hint check?
3838

39-
The hint checks if all scripts and stylesheets are served with the
39+
The hint checks if all resources are served with the
4040
`X-Content-Type-Options` HTTP headers with the value of `nosniff`.
4141

4242
### Examples that **trigger** the hint
4343

44-
Resource that is not script or stylesheet is served with the
44+
Resource is not served with the
4545
`X-Content-Type-Options` HTTP header.
4646

4747
```text
@@ -50,7 +50,6 @@ HTTP/... 200 OK
5050
...
5151
5252
Content-Type: image/png
53-
X-Content-Type-Options: nosniff
5453
```
5554

5655
Script is served with the `X-Content-Type-Options` HTTP header
@@ -77,6 +76,69 @@ Content-Type: text/javascript; charset=utf-8
7776
X-Content-Type-Options: nosniff
7877
```
7978

79+
## How to configure the server to pass this hint
80+
81+
<details><summary>How to configure Apache</summary>
82+
83+
Apache can be configured to add headers using the [`Header`
84+
directive][header directive].
85+
86+
```apache
87+
<IfModule mod_headers.c>
88+
Header always set X-Content-Type-Options nosniff
89+
</IfModule>
90+
```
91+
92+
Note that:
93+
94+
* The above snippet works with Apache `v2.2.0+`, but you need to have
95+
[`mod_headers`][mod_headers] [enabled][how to enable apache modules]
96+
for it to take effect.
97+
98+
* If you have access to the [main Apache configuration file][main
99+
apache conf file] (usually called `httpd.conf`), you should add
100+
the logic in, for example, a [`<Directory>`][apache directory]
101+
section in that file. This is usually the recommended way as
102+
[using `.htaccess` files slows down][htaccess is slow] Apache!
103+
104+
If you don't have access to the main configuration file (quite
105+
common with hosting services), add the snippets in a `.htaccess`
106+
file in the root of the web site/app.
107+
108+
For the complete set of configurations, not just for this rule, see
109+
the [Apache server configuration related documentation][apache config].
110+
111+
</details>
112+
113+
<details>
114+
115+
<summary>How to configure IIS</summary>
116+
117+
You can add this header unconditionally to all responses.
118+
119+
```xml
120+
<configuration>
121+
<system.webServer>
122+
<httpProtocol>
123+
<customHeaders>
124+
<add name="X-Content-Type-Options" value="nosniff" />
125+
</customHeaders>
126+
</httpProtocol>
127+
</system.webServer>
128+
</configuration>
129+
```
130+
131+
Note that:
132+
133+
* The above snippet works with IIS 7+.
134+
* You should use the above snippet in the `web.config` of your
135+
application.
136+
137+
For the complete set of configurations, not just for this rule,
138+
see the [IIS server configuration related documentation][iis config].
139+
140+
</details>
141+
80142
## How to use this hint?
81143

82144
To use it you will have to install it via `npm`:
@@ -115,22 +177,24 @@ And then activate it via the [`.hintrc`][hintrc] configuration file:
115177

116178
<!-- Link labels: -->
117179

180+
[chromium corb]: https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md
181+
[chromium ssca]: https://www.chromium.org/Home/chromium-security/ssca
118182
[fetch spec blocking]: https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff%3F
119183
[fetch spec issue]: https://github.com/whatwg/fetch/issues/395
184+
[hintrc]: https://webhint.io/docs/user-guide/configuring-webhint/summary/
120185
[javascript media types]: https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type
121186
[mime sniffing spec]: https://mimesniff.spec.whatwg.org/
122-
[hintrc]: https://webhint.io/docs/user-guide/configuring-webhint/summary/
123187

124188
<!-- Apache links -->
125189

190+
[apache config]: https://webhint.io/docs/user-guide/server-configurations/apache/
126191
[apache directory]: https://httpd.apache.org/docs/current/mod/core.html#directory
127192
[header directive]: https://httpd.apache.org/docs/current/mod/mod_headers.html#header
128193
[how to enable apache modules]: https://github.com/h5bp/server-configs-apache/tree/7eb30da6a06ec4fc24daf33c75b7bd86f9ad1f68#enable-apache-httpd-modules
129194
[htaccess is slow]: https://httpd.apache.org/docs/current/howto/htaccess.html#when
130195
[main apache conf file]: https://httpd.apache.org/docs/current/configuring.html#main
131196
[mod_headers]: https://httpd.apache.org/docs/current/mod/mod_headers.html
132-
[mod_mime]: https://httpd.apache.org/docs/current/mod/mod_mime.html
133197

134198
<!-- IIS links -->
135199

136-
[url rewrite]: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-the-url-rewrite-module
200+
[iis config]: https://webhint.io/docs/user-guide/server-configurations/iis/

packages/hint-x-content-type-options/src/hint.ts

+8-44
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111

1212
import { debug as d } from 'hint/dist/src/lib/utils/debug';
13-
import { IAsyncHTMLElement, FetchEnd, IHint } from 'hint/dist/src/lib/types';
13+
import { FetchEnd, IHint } from 'hint/dist/src/lib/types';
1414
import normalizeString from 'hint/dist/src/lib/utils/misc/normalize-string';
1515
import isDataURI from 'hint/dist/src/lib/utils/network/is-data-uri';
1616
import { HintContext } from 'hint/dist/src/lib/hint-context';
@@ -28,36 +28,6 @@ export default class XContentTypeOptionsHint implements IHint {
2828

2929
public static readonly meta = meta;
3030
public constructor(context: HintContext) {
31-
32-
const isHeaderRequired = (element: IAsyncHTMLElement | null): boolean => {
33-
if (!element) {
34-
return false;
35-
}
36-
37-
const nodeName = normalizeString(element.nodeName);
38-
39-
/*
40-
* See:
41-
*
42-
* * https://github.com/whatwg/fetch/issues/395
43-
* * https://fetch.spec.whatwg.org/#x-content-type-options-header
44-
*/
45-
46-
if (nodeName === 'script') {
47-
return true;
48-
49-
}
50-
51-
if (nodeName === 'link') {
52-
// We check if element exists before and `normalizeString` will return `''` as default
53-
const relValues = (normalizeString(element.getAttribute('rel'), ''))!.split(' ');
54-
55-
return relValues.includes('stylesheet');
56-
}
57-
58-
return false;
59-
};
60-
6131
const validate = async ({ element, resource, response }: FetchEnd) => {
6232
// This check does not make sense for data URI.
6333

@@ -69,25 +39,19 @@ export default class XContentTypeOptionsHint implements IHint {
6939

7040
const headerValue: string | null = normalizeString(response.headers && response.headers['x-content-type-options']);
7141

72-
if (isHeaderRequired(element)) {
73-
if (headerValue === null) {
74-
await context.report(resource, `Response should include 'x-content-type-options' header.`, { element });
42+
if (headerValue === null) {
43+
await context.report(resource, `Response should include 'x-content-type-options' header.`, { element });
7544

76-
return;
77-
}
78-
79-
if (headerValue !== 'nosniff') {
80-
await context.report(resource, `'x-content-type-options' header value should be 'nosniff', not '${headerValue}'.`, { element });
45+
return;
46+
}
8147

82-
return;
83-
}
48+
if (headerValue !== 'nosniff') {
49+
await context.report(resource, `'x-content-type-options' header value should be 'nosniff', not '${headerValue}'.`, { element });
8450

8551
return;
8652
}
8753

88-
if (headerValue) {
89-
await context.report(resource, `Response should not include unneeded 'x-content-type-options' header.`, { element });
90-
}
54+
return;
9155
};
9256

9357
context.on('fetch::end::*', validate);

packages/hint-x-content-type-options/tests/tests.ts

+28-17
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,18 @@ import { getHintPath } from 'hint/dist/src/lib/utils/hint-helpers';
33
import { HintTest } from '@hint/utils-tests-helpers/dist/src/hint-test-type';
44
import * as hintRunner from '@hint/utils-tests-helpers/dist/src/hint-runner';
55

6-
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
7-
86
// Page data.
97

108
const pageWithAlternateStylesheet = generateHTMLPage('<link rel=" alternate stylesheet " href="test.css">');
119
const pageWithScript = generateHTMLPage(undefined, '<script src="test.js"></script>');
10+
const pageWithScriptAndStylesheet = generateHTMLPage('<link rel="stylesheet" href="test.css">', '<script src="test.js"></script>');
1211
const pageWithStylesheet = generateHTMLPage('<link rel="stylesheet" href="test.css">');
1312

13+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
14+
1415
// Error messages.
1516

1617
const noHeaderErrorMessage = `Response should include 'x-content-type-options' header.`;
17-
const unneededHeaderErrorMessage = `Response should not include unneeded 'x-content-type-options' header.`;
18-
1918
const generateInvalidValueMessage = (value: string = '') => {
2019
return `'x-content-type-options' header value should be 'nosniff', not '${value}'.`;
2120
};
@@ -27,48 +26,60 @@ const generateInvalidValueMessage = (value: string = '') => {
2726
const tests: HintTest[] = [
2827
{
2928
name: `HTML page is served without 'X-Content-Type-Options' header`,
30-
serverConfig: { '/': '' }
29+
reports: [{ message: noHeaderErrorMessage }],
30+
serverConfig: {
31+
'/': { content: generateHTMLPage() },
32+
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } }
33+
}
3134
},
3235
{
33-
name: `Script is served without 'X-Content-Type-Options' header`,
36+
name: `Favicon is served without 'X-Content-Type-Options' header`,
3437
reports: [{ message: noHeaderErrorMessage }],
3538
serverConfig: {
36-
'/': pageWithScript,
37-
'/test.js': ''
39+
'/': { content: generateHTMLPage(), headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
40+
'/favicon.ico': ''
3841
}
3942
},
4043
{
4144
name: `Stylesheet is served without 'X-Content-Type-Options' header`,
4245
reports: [{ message: noHeaderErrorMessage }],
4346
serverConfig: {
44-
'/': pageWithStylesheet,
47+
'/': { content: pageWithStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
48+
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } },
4549
'/test.css': ''
4650
}
4751
},
4852
{
4953
name: `Alternate stylesheet is served without 'X-Content-Type-Options' header`,
5054
reports: [{ message: noHeaderErrorMessage }],
5155
serverConfig: {
52-
'/': pageWithAlternateStylesheet,
56+
'/': { content: pageWithAlternateStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
57+
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } },
5358
'/test.css': ''
5459
}
5560
},
5661
{
5762
name: `Resource is specified as a data URI`,
58-
serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') }
59-
},
60-
{
61-
name: `HTML page is served with the 'X-Content-Type-Options' header`,
62-
reports: [{ message: unneededHeaderErrorMessage }],
63-
serverConfig: { '/': { headers: { 'X-Content-Type-Options': 'nosniff' } } }
63+
serverConfig: {
64+
'/': {
65+
content: generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">'),
66+
headers: { 'X-Content-Type-Options': 'nosniff' }
67+
},
68+
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } }
69+
}
6470
},
6571
{
6672
name: `Script is served with 'X-Content-Type-Options' header with invalid value`,
6773
reports: [{ message: generateInvalidValueMessage('invalid') }],
6874
serverConfig: {
69-
'/': pageWithScript,
75+
'/': { content: pageWithScript, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
76+
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } },
7077
'/test.js': { headers: { 'X-Content-Type-Options': 'invalid' } }
7178
}
79+
},
80+
{
81+
name: `All resources are served with 'X-Content-Type-Options' header`,
82+
serverConfig: { '*': { content: pageWithScriptAndStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } } }
7283
}
7384
];
7485

0 commit comments

Comments
 (0)