Skip to content

Commit 0920125

Browse files
committed
New: 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. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Ref webhintio#1221 Close webhintio#1221
1 parent 36b5646 commit 0920125

File tree

3 files changed

+46
-70
lines changed

3 files changed

+46
-70
lines changed

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

+10-9
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
@@ -116,6 +115,8 @@ And then activate it via the [`.hintrc`][hintrc] configuration file:
116115
<!-- Link labels: -->
117116

118117
[fetch spec blocking]: https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff%3F
118+
[chromium ssca]: https://www.chromium.org/Home/chromium-security/ssca
119+
[chromium corb]: https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md
119120
[fetch spec issue]: https://github.com/whatwg/fetch/issues/395
120121
[javascript media types]: https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type
121122
[mime sniffing spec]: https://mimesniff.spec.whatwg.org/

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)