Skip to content

Commit fbb8ff3

Browse files
sarvajeantross
authored andcommitted
Fix: Treat XML like HTML and allow CSP headers for scripts
Fix #2349 Fix #2342 Close #2618
1 parent 69acc22 commit fbb8ff3

File tree

3 files changed

+120
-52
lines changed

3 files changed

+120
-52
lines changed

packages/hint-no-html-only-headers/README.md

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Unneeded HTTP headers (`no-html-only-headers`)
22

33
`no-html-only-headers` warns against responding with HTTP headers that
4-
are not needed for non-HTML resources.
4+
are not needed for non-HTML (or non-XML) resources.
55

66
## Why is this important?
77

@@ -16,11 +16,14 @@ HTTP headers:
1616

1717
* `Content-Security-Policy`
1818
* `X-Content-Security-Policy`
19-
* `X-Frame-Options`
2019
* `X-UA-Compatible`
2120
* `X-WebKit-CSP`
2221
* `X-XSS-Protection`
2322

23+
In case of a JavaScript file, `Content-Security-Policy` and
24+
`X-Content-Security-Policy` will be ignored since CSP is
25+
also relevant to workers.
26+
2427
### Examples that **trigger** the hint
2528

2629
Response for `/test.js`:
@@ -30,9 +33,6 @@ HTTP/... 200 OK
3033
3134
Content-Type: text/javascript; charset=utf-8
3235
...
33-
Content-Security-Policy: default-src 'none'
34-
X-Content-Security-Policy: default-src 'none'
35-
X-Frame-Options: DENY
3636
X-UA-Compatible: IE=Edge,
3737
X-WebKit-CSP: default-src 'none'
3838
X-XSS-Protection: 1; mode=block
@@ -48,7 +48,6 @@ Content-Type: x/y
4848
...
4949
Content-Security-Policy: default-src 'none'
5050
X-Content-Security-Policy: default-src 'none'
51-
X-Frame-Options: DENY
5251
X-UA-Compatible: IE=Edge,
5352
X-WebKit-CSP: default-src 'none'
5453
X-XSS-Protection: 1; mode=block
@@ -63,6 +62,8 @@ Response for `/test.js`:
6362
HTTP/... 200 OK
6463
6564
Content-Type: text/javascript; charset=utf-8
65+
Content-Security-Policy: default-src 'none'
66+
X-Content-Security-Policy: default-src 'none'
6667
...
6768
```
6869

@@ -75,7 +76,21 @@ Content-Type: text/html
7576
...
7677
Content-Security-Policy: default-src 'none'
7778
X-Content-Security-Policy: default-src 'none'
78-
X-Frame-Options: DENY
79+
X-UA-Compatible: IE=Edge,
80+
X-WebKit-CSP: default-src 'none'
81+
X-XSS-Protection: 1; mode=block
82+
...
83+
```
84+
85+
Response for `/test.xml`:
86+
87+
```text
88+
HTTP/... 200 OK
89+
90+
Content-Type: application/xhtml+xml
91+
...
92+
Content-Security-Policy: default-src 'none'
93+
X-Content-Security-Policy: default-src 'none'
7994
X-UA-Compatible: IE=Edge,
8095
X-WebKit-CSP: default-src 'none'
8196
X-XSS-Protection: 1; mode=block
@@ -98,13 +113,15 @@ you can do something such as the following:
98113
# Because `mod_headers` cannot match based on the content-type,
99114
# the following workaround needs to be used.
100115
101-
<FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ic[os]|jpe?g|m?js|json(ld)?|m4[av]|manifest|map|markdown|md|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$">
116+
<FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ic[os]|jpe?g|m?js|json(ld)?|m4[av]|manifest|map|markdown|md|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xpi)$">
117+
Header unset X-UA-Compatible
118+
Header unset X-XSS-Protection
119+
</FilesMatch>
120+
121+
<FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ic[os]|jpe?g|json(ld)?|m4[av]|manifest|map|markdown|md|mp4|oex|og[agv]|opus|otf|png|rdf|rss|safariextz|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xpi)$">
102122
Header unset Content-Security-Policy
103123
Header unset X-Content-Security-Policy
104-
Header unset X-Frame-Options
105-
Header unset X-UA-Compatible
106124
Header unset X-WebKit-CSP
107-
Header unset X-XSS-Protection
108125
</FilesMatch>
109126
</IfModule>
110127
```
@@ -143,21 +160,14 @@ any resource whose `Content-Type` header isn't `text/html`:
143160
<rule name="Content-Security-Policy">
144161
<match serverVariable="RESPONSE_Content_Security_Policy" pattern=".*" />
145162
<conditions>
146-
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
163+
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^(text/html|text/xml|application/xhtml+xml|text/javascript|application/pdf|image/svg+xml)" negate="true" />
147164
</conditions>
148165
<action type="Rewrite" value=""/>
149166
</rule>
150167
<rule name="X-Content-Security-Policy">
151168
<match serverVariable="RESPONSE_X_Content_Security_Policy" pattern=".*" />
152169
<conditions>
153-
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
154-
</conditions>
155-
<action type="Rewrite" value=""/>
156-
</rule>
157-
<rule name="X-Frame-Options">
158-
<match serverVariable="RESPONSE_X_Frame_Options" pattern=".*" />
159-
<conditions>
160-
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
170+
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^(text/html|text/xml|application/xhtml+xml|text/javascript|application/pdf|image/svg+xml)" negate="true" />
161171
</conditions>
162172
<action type="Rewrite" value=""/>
163173
</rule>
@@ -171,7 +181,7 @@ any resource whose `Content-Type` header isn't `text/html`:
171181
<rule name="X-WebKit-CSP">
172182
<match serverVariable="RESPONSE_X_Webkit_csp" pattern=".*" />
173183
<conditions>
174-
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
184+
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^(text/html|text/xml|application/xhtml+xml|text/javascript|application/pdf|image/svg+xml)" negate="true" />
175185
</conditions>
176186
<action type="Rewrite" value=""/>
177187
</rule>

packages/hint-no-html-only-headers/src/hint.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,29 @@ export default class NoHtmlOnlyHeadersHint implements IHint {
3333

3434
public constructor(context: HintContext) {
3535

36-
let unneededHeaders: string[] = [
36+
let unneededHeaders = [
3737
'content-security-policy',
3838
'feature-policy',
3939
'x-content-security-policy',
40-
'x-frame-options',
4140
'x-ua-compatible',
4241
'x-webkit-csp',
4342
'x-xss-protection'
4443
];
4544

45+
// TODO: Remove once https://github.com/webhintio/hint/issues/25 is implemented.
46+
const exceptionHeaders = [
47+
'content-security-policy',
48+
'x-content-security-policy',
49+
'x-webkit-csp'
50+
];
51+
52+
// TODO: Remove once https://github.com/webhintio/hint/issues/25 is implemented.
53+
const exceptionMediaTypes = [
54+
'application/pdf',
55+
'image/svg+xml',
56+
'text/javascript'
57+
];
58+
4659
const loadHintConfigs = () => {
4760
const includeHeaders = (context.hintOptions && context.hintOptions.include) || [];
4861
const ignoreHeaders = (context.hintOptions && context.hintOptions.ignore) || [];
@@ -51,8 +64,8 @@ export default class NoHtmlOnlyHeadersHint implements IHint {
5164
};
5265

5366
const willBeTreatedAsHTML = (response: Response): boolean => {
54-
const contentTypeHeader: string | undefined = response.headers['content-type'];
55-
const mediaType: string = contentTypeHeader ? contentTypeHeader.split(';')[0].trim() : '';
67+
const contentTypeHeader = response.headers['content-type'];
68+
const mediaType = contentTypeHeader ? contentTypeHeader.split(';')[0].trim() : '';
5669

5770
/*
5871
* By default, browsers will treat resource sent with the
@@ -61,6 +74,7 @@ export default class NoHtmlOnlyHeadersHint implements IHint {
6174

6275
if ([
6376
'text/html',
77+
'text/xml',
6478
'application/xhtml+xml'
6579
].includes(mediaType)) {
6680
return true;
@@ -101,8 +115,13 @@ export default class NoHtmlOnlyHeadersHint implements IHint {
101115
}
102116

103117
if (!willBeTreatedAsHTML(response)) {
104-
const headers: string[] = includedHeaders(response.headers, unneededHeaders);
105-
const numberOfHeaders: number = headers.length;
118+
let headersToValidate = unneededHeaders;
119+
120+
if (exceptionMediaTypes.includes(response.mediaType)) {
121+
headersToValidate = mergeIgnoreIncludeArrays(headersToValidate, exceptionHeaders, []);
122+
}
123+
const headers = includedHeaders(response.headers, headersToValidate);
124+
const numberOfHeaders = headers.length;
106125

107126
if (numberOfHeaders > 0) {
108127
const message = `Response should not include unneeded ${prettyPrintArray(headers)} ${numberOfHeaders === 1 ? 'header' : 'headers'}.`;

packages/hint-no-html-only-headers/tests/tests.ts

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,95 @@ const testsForDefaults: HintTest[] = [
1515
name: `Non HTML resource is served without unneeded headers`,
1616
serverConfig: {
1717
'/': {
18-
content: htmlPage,
18+
content: generateHTMLPage(undefined, '<img src="test.svg"/><script src="test.js"></script><embed src="test.pdf" type="application/pdf">'),
1919
headers: {
20+
'Content-Security-Policy': 'default-src "none"',
2021
'Content-Type': 'text/html; charset=utf-8',
21-
'X-Frame-Options': 'SAMEORIGIN'
22+
'X-Content-Security-Policy': 'default-src "none"',
23+
'X-WebKit-CSP': 'default-src "none"'
24+
}
25+
},
26+
'/test.js': {
27+
headers: {
28+
'Content-Security-Policy': 'default-src "none"',
29+
'Content-Type': 'application/javascript; charset=utf-8',
30+
'X-Content-Security-Policy': 'default-src "none"',
31+
'X-WebKit-CSP': 'default-src "none"'
32+
}
33+
},
34+
'/test.pdf': {
35+
headers: {
36+
'Content-Security-Policy': 'default-src "none"',
37+
'Content-Type': 'application/pdf',
38+
'X-Content-Security-Policy': 'default-src "none"',
39+
'X-WebKit-CSP': 'default-src "none"'
2240
}
2341
},
24-
'/test.js': { headers: { 'Content-Type': 'application/javascript; charset=utf-8' } }
42+
'/test.svg': {
43+
headers: {
44+
'Content-Security-Policy': 'default-src "none"',
45+
'Content-Type': 'image/svg+xml',
46+
'X-Content-Security-Policy': 'default-src "none"',
47+
'X-WebKit-CSP': 'default-src "none"'
48+
}
49+
}
2550
}
2651
},
2752
{
28-
name: `Non HTML resource is specified as a data URI`,
29-
serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') }
53+
name: `Non HTML resource is served without unneeded headers and with application/xhtml+xml content type`,
54+
serverConfig: {
55+
'/': {
56+
content: generateHTMLPage(undefined, '<script src="test.js"></script>'),
57+
headers: {
58+
'Content-Security-Policy': 'default-src "none"',
59+
'Content-Type': 'application/xhtml+xml; charset=utf-8',
60+
'X-Content-Security-Policy': 'default-src "none"',
61+
'X-WebKit-CSP': 'default-src "none"'
62+
}
63+
},
64+
'/test.js': {
65+
headers: {
66+
'Content-Security-Policy': 'default-src "none"',
67+
'Content-Type': 'application/javascript; charset=utf-8',
68+
'X-Content-Security-Policy': 'default-src "none"',
69+
'X-WebKit-CSP': 'default-src "none"'
70+
}
71+
}
72+
}
3073
},
3174
{
32-
name: `Non HTML resource is served with unneeded header`,
33-
reports: [{ message: generateMessage(['content-security-policy']) }],
75+
name: `Non HTML resource is served without unneeded headers and with text/xml content type`,
3476
serverConfig: {
3577
'/': {
36-
content: htmlPage,
78+
content: generateHTMLPage(undefined, '<script src="test.js"></script>'),
3779
headers: {
38-
'Content-Type': 'text/html; charset=utf-8',
39-
'X-Frame-Options': 'SAMEORIGIN'
80+
'Content-Security-Policy': 'default-src "none"',
81+
'Content-Type': 'text/xml; charset=utf-8',
82+
'X-Content-Security-Policy': 'default-src "none"',
83+
'X-WebKit-CSP': 'default-src "none"'
4084
}
4185
},
4286
'/test.js': {
4387
headers: {
4488
'Content-Security-Policy': 'default-src "none"',
45-
'Content-Type': 'application/javascript; charset=utf-8'
89+
'Content-Type': 'application/javascript; charset=utf-8',
90+
'X-Content-Security-Policy': 'default-src "none"',
91+
'X-WebKit-CSP': 'default-src "none"'
4692
}
4793
}
4894
}
4995
},
96+
{
97+
name: `Non HTML resource is specified as a data URI`,
98+
serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') }
99+
},
50100
{
51101
name: `Non HTML resource is served with multiple unneeded headers`,
52102
reports: [
53103
{
54104
message: generateMessage([
55-
'content-security-policy',
56105
'feature-policy',
57-
'x-content-security-policy',
58-
'x-frame-options',
59106
'x-ua-compatible',
60-
'x-webkit-csp',
61107
'x-xss-protection'
62108
])
63109
}
@@ -68,7 +114,6 @@ const testsForDefaults: HintTest[] = [
68114
headers: {
69115
'Content-Type': 'text/html; charset=utf-8',
70116
'X-Content-Security-Policy': 'default-src "none"',
71-
'X-Frame-Options': 'DENY',
72117
'X-UA-Compatible': 'IE=Edge',
73118
'X-WebKit-CSP': 'default-src "none"',
74119
'X-XSS-Protection': '1; mode=block'
@@ -80,7 +125,6 @@ const testsForDefaults: HintTest[] = [
80125
'Content-Type': 'application/javascript; charset=utf-8',
81126
'Feature-Policy': `geolocation 'self'`,
82127
'X-Content-Security-Policy': 'default-src "none"',
83-
'X-Frame-Options': 'DENY',
84128
'X-UA-Compatible': 'IE=Edge',
85129
'X-WebKit-CSP': 'default-src "none"',
86130
'X-XSS-Protection': '1; mode=block'
@@ -138,7 +182,6 @@ const testsForIgnoreConfigs: HintTest[] = [
138182
headers: {
139183
'Content-Type': 'text/html; charset=utf-8',
140184
'Feature-Policy': `geolocation 'self'`,
141-
'X-Frame-Options': 'SAMEORIGIN',
142185
'X-UA-Compatible': 'IE=Edge'
143186
}
144187
},
@@ -159,7 +202,6 @@ const testsForIncludeConfigs: HintTest[] = [
159202
reports: [
160203
{
161204
message: generateMessage([
162-
'content-security-policy',
163205
'x-test-1',
164206
'x-ua-compatible'
165207
])
@@ -170,7 +212,6 @@ const testsForIncludeConfigs: HintTest[] = [
170212
content: htmlPage,
171213
headers: {
172214
'Content-Type': 'text/html; charset=utf-8',
173-
'X-Frame-Options': 'SAMEORIGIN',
174215
'X-Test-1': 'test',
175216
'X-Test-2': 'test'
176217
}
@@ -193,7 +234,6 @@ const testsForConfigs: HintTest[] = [
193234
reports: [
194235
{
195236
message: generateMessage([
196-
'content-security-policy',
197237
'x-test-1',
198238
'x-ua-compatible'
199239
])
@@ -204,7 +244,6 @@ const testsForConfigs: HintTest[] = [
204244
content: htmlPage,
205245
headers: {
206246
'Content-Type': 'text/html; charset=utf-8',
207-
'X-Frame-Options': 'SAMEORIGIN',
208247
'X-Test-1': 'test',
209248
'X-Test-2': 'test'
210249
}
@@ -223,11 +262,11 @@ const testsForConfigs: HintTest[] = [
223262
];
224263

225264
testHint(hintPath, testsForDefaults);
226-
testHint(hintPath, testsForIgnoreConfigs, { hintOptions: { ignore: ['Content-Security-Policy', 'X-UA-Compatible', 'X-Test-1'] } });
227-
testHint(hintPath, testsForIncludeConfigs, { hintOptions: { include: ['Content-Security-Policy', 'X-Test-1', 'X-Test-2'] } });
265+
testHint(hintPath, testsForIgnoreConfigs, { hintOptions: { ignore: ['X-UA-Compatible', 'X-Test-1'] } });
266+
testHint(hintPath, testsForIncludeConfigs, { hintOptions: { include: ['X-Test-1', 'X-Test-2'] } });
228267
testHint(hintPath, testsForConfigs, {
229268
hintOptions: {
230-
ignore: ['X-Frame-Options', 'X-Test-2', 'X-Test-3'],
269+
ignore: ['X-Test-2', 'X-Test-3'],
231270
include: ['X-Test-1', 'X-Test-2', 'X-UA-Compatible']
232271
}
233272
});

0 commit comments

Comments
 (0)