Skip to content

Commit

Permalink
Fix: Treat XML like HTML and allow CSP headers for scripts
Browse files Browse the repository at this point in the history
Fix #2349
Fix #2342
Close #2618
  • Loading branch information
sarvaje authored and antross committed Jun 21, 2019
1 parent 69acc22 commit fbb8ff3
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 52 deletions.
52 changes: 31 additions & 21 deletions packages/hint-no-html-only-headers/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Unneeded HTTP headers (`no-html-only-headers`)

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

## Why is this important?

Expand All @@ -16,11 +16,14 @@ HTTP headers:

* `Content-Security-Policy`
* `X-Content-Security-Policy`
* `X-Frame-Options`
* `X-UA-Compatible`
* `X-WebKit-CSP`
* `X-XSS-Protection`

In case of a JavaScript file, `Content-Security-Policy` and
`X-Content-Security-Policy` will be ignored since CSP is
also relevant to workers.

### Examples that **trigger** the hint

Response for `/test.js`:
Expand All @@ -30,9 +33,6 @@ HTTP/... 200 OK
Content-Type: text/javascript; charset=utf-8
...
Content-Security-Policy: default-src 'none'
X-Content-Security-Policy: default-src 'none'
X-Frame-Options: DENY
X-UA-Compatible: IE=Edge,
X-WebKit-CSP: default-src 'none'
X-XSS-Protection: 1; mode=block
Expand All @@ -48,7 +48,6 @@ Content-Type: x/y
...
Content-Security-Policy: default-src 'none'
X-Content-Security-Policy: default-src 'none'
X-Frame-Options: DENY
X-UA-Compatible: IE=Edge,
X-WebKit-CSP: default-src 'none'
X-XSS-Protection: 1; mode=block
Expand All @@ -63,6 +62,8 @@ Response for `/test.js`:
HTTP/... 200 OK
Content-Type: text/javascript; charset=utf-8
Content-Security-Policy: default-src 'none'
X-Content-Security-Policy: default-src 'none'
...
```

Expand All @@ -75,7 +76,21 @@ Content-Type: text/html
...
Content-Security-Policy: default-src 'none'
X-Content-Security-Policy: default-src 'none'
X-Frame-Options: DENY
X-UA-Compatible: IE=Edge,
X-WebKit-CSP: default-src 'none'
X-XSS-Protection: 1; mode=block
...
```

Response for `/test.xml`:

```text
HTTP/... 200 OK
Content-Type: application/xhtml+xml
...
Content-Security-Policy: default-src 'none'
X-Content-Security-Policy: default-src 'none'
X-UA-Compatible: IE=Edge,
X-WebKit-CSP: default-src 'none'
X-XSS-Protection: 1; mode=block
Expand All @@ -98,13 +113,15 @@ you can do something such as the following:
# Because `mod_headers` cannot match based on the content-type,
# the following workaround needs to be used.
<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)$">
<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)$">
Header unset X-UA-Compatible
Header unset X-XSS-Protection
</FilesMatch>
<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)$">
Header unset Content-Security-Policy
Header unset X-Content-Security-Policy
Header unset X-Frame-Options
Header unset X-UA-Compatible
Header unset X-WebKit-CSP
Header unset X-XSS-Protection
</FilesMatch>
</IfModule>
```
Expand Down Expand Up @@ -143,21 +160,14 @@ any resource whose `Content-Type` header isn't `text/html`:
<rule name="Content-Security-Policy">
<match serverVariable="RESPONSE_Content_Security_Policy" pattern=".*" />
<conditions>
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^(text/html|text/xml|application/xhtml+xml|text/javascript|application/pdf|image/svg+xml)" negate="true" />
</conditions>
<action type="Rewrite" value=""/>
</rule>
<rule name="X-Content-Security-Policy">
<match serverVariable="RESPONSE_X_Content_Security_Policy" pattern=".*" />
<conditions>
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
</conditions>
<action type="Rewrite" value=""/>
</rule>
<rule name="X-Frame-Options">
<match serverVariable="RESPONSE_X_Frame_Options" pattern=".*" />
<conditions>
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^(text/html|text/xml|application/xhtml+xml|text/javascript|application/pdf|image/svg+xml)" negate="true" />
</conditions>
<action type="Rewrite" value=""/>
</rule>
Expand All @@ -171,7 +181,7 @@ any resource whose `Content-Type` header isn't `text/html`:
<rule name="X-WebKit-CSP">
<match serverVariable="RESPONSE_X_Webkit_csp" pattern=".*" />
<conditions>
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" negate="true" />
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^(text/html|text/xml|application/xhtml+xml|text/javascript|application/pdf|image/svg+xml)" negate="true" />
</conditions>
<action type="Rewrite" value=""/>
</rule>
Expand Down
31 changes: 25 additions & 6 deletions packages/hint-no-html-only-headers/src/hint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,29 @@ export default class NoHtmlOnlyHeadersHint implements IHint {

public constructor(context: HintContext) {

let unneededHeaders: string[] = [
let unneededHeaders = [
'content-security-policy',
'feature-policy',
'x-content-security-policy',
'x-frame-options',
'x-ua-compatible',
'x-webkit-csp',
'x-xss-protection'
];

// TODO: Remove once https://github.com/webhintio/hint/issues/25 is implemented.
const exceptionHeaders = [
'content-security-policy',
'x-content-security-policy',
'x-webkit-csp'
];

// TODO: Remove once https://github.com/webhintio/hint/issues/25 is implemented.
const exceptionMediaTypes = [
'application/pdf',
'image/svg+xml',
'text/javascript'
];

const loadHintConfigs = () => {
const includeHeaders = (context.hintOptions && context.hintOptions.include) || [];
const ignoreHeaders = (context.hintOptions && context.hintOptions.ignore) || [];
Expand All @@ -51,8 +64,8 @@ export default class NoHtmlOnlyHeadersHint implements IHint {
};

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

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

if ([
'text/html',
'text/xml',
'application/xhtml+xml'
].includes(mediaType)) {
return true;
Expand Down Expand Up @@ -101,8 +115,13 @@ export default class NoHtmlOnlyHeadersHint implements IHint {
}

if (!willBeTreatedAsHTML(response)) {
const headers: string[] = includedHeaders(response.headers, unneededHeaders);
const numberOfHeaders: number = headers.length;
let headersToValidate = unneededHeaders;

if (exceptionMediaTypes.includes(response.mediaType)) {
headersToValidate = mergeIgnoreIncludeArrays(headersToValidate, exceptionHeaders, []);
}
const headers = includedHeaders(response.headers, headersToValidate);
const numberOfHeaders = headers.length;

if (numberOfHeaders > 0) {
const message = `Response should not include unneeded ${prettyPrintArray(headers)} ${numberOfHeaders === 1 ? 'header' : 'headers'}.`;
Expand Down
89 changes: 64 additions & 25 deletions packages/hint-no-html-only-headers/tests/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,95 @@ const testsForDefaults: HintTest[] = [
name: `Non HTML resource is served without unneeded headers`,
serverConfig: {
'/': {
content: htmlPage,
content: generateHTMLPage(undefined, '<img src="test.svg"/><script src="test.js"></script><embed src="test.pdf" type="application/pdf">'),
headers: {
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'text/html; charset=utf-8',
'X-Frame-Options': 'SAMEORIGIN'
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
},
'/test.js': {
headers: {
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'application/javascript; charset=utf-8',
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
},
'/test.pdf': {
headers: {
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'application/pdf',
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
},
'/test.js': { headers: { 'Content-Type': 'application/javascript; charset=utf-8' } }
'/test.svg': {
headers: {
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'image/svg+xml',
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
}
}
},
{
name: `Non HTML resource is specified as a data URI`,
serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') }
name: `Non HTML resource is served without unneeded headers and with application/xhtml+xml content type`,
serverConfig: {
'/': {
content: generateHTMLPage(undefined, '<script src="test.js"></script>'),
headers: {
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'application/xhtml+xml; charset=utf-8',
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
},
'/test.js': {
headers: {
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'application/javascript; charset=utf-8',
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
}
}
},
{
name: `Non HTML resource is served with unneeded header`,
reports: [{ message: generateMessage(['content-security-policy']) }],
name: `Non HTML resource is served without unneeded headers and with text/xml content type`,
serverConfig: {
'/': {
content: htmlPage,
content: generateHTMLPage(undefined, '<script src="test.js"></script>'),
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-Frame-Options': 'SAMEORIGIN'
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'text/xml; charset=utf-8',
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
},
'/test.js': {
headers: {
'Content-Security-Policy': 'default-src "none"',
'Content-Type': 'application/javascript; charset=utf-8'
'Content-Type': 'application/javascript; charset=utf-8',
'X-Content-Security-Policy': 'default-src "none"',
'X-WebKit-CSP': 'default-src "none"'
}
}
}
},
{
name: `Non HTML resource is specified as a data URI`,
serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') }
},
{
name: `Non HTML resource is served with multiple unneeded headers`,
reports: [
{
message: generateMessage([
'content-security-policy',
'feature-policy',
'x-content-security-policy',
'x-frame-options',
'x-ua-compatible',
'x-webkit-csp',
'x-xss-protection'
])
}
Expand All @@ -68,7 +114,6 @@ const testsForDefaults: HintTest[] = [
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-Content-Security-Policy': 'default-src "none"',
'X-Frame-Options': 'DENY',
'X-UA-Compatible': 'IE=Edge',
'X-WebKit-CSP': 'default-src "none"',
'X-XSS-Protection': '1; mode=block'
Expand All @@ -80,7 +125,6 @@ const testsForDefaults: HintTest[] = [
'Content-Type': 'application/javascript; charset=utf-8',
'Feature-Policy': `geolocation 'self'`,
'X-Content-Security-Policy': 'default-src "none"',
'X-Frame-Options': 'DENY',
'X-UA-Compatible': 'IE=Edge',
'X-WebKit-CSP': 'default-src "none"',
'X-XSS-Protection': '1; mode=block'
Expand Down Expand Up @@ -138,7 +182,6 @@ const testsForIgnoreConfigs: HintTest[] = [
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Feature-Policy': `geolocation 'self'`,
'X-Frame-Options': 'SAMEORIGIN',
'X-UA-Compatible': 'IE=Edge'
}
},
Expand All @@ -159,7 +202,6 @@ const testsForIncludeConfigs: HintTest[] = [
reports: [
{
message: generateMessage([
'content-security-policy',
'x-test-1',
'x-ua-compatible'
])
Expand All @@ -170,7 +212,6 @@ const testsForIncludeConfigs: HintTest[] = [
content: htmlPage,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-Frame-Options': 'SAMEORIGIN',
'X-Test-1': 'test',
'X-Test-2': 'test'
}
Expand All @@ -193,7 +234,6 @@ const testsForConfigs: HintTest[] = [
reports: [
{
message: generateMessage([
'content-security-policy',
'x-test-1',
'x-ua-compatible'
])
Expand All @@ -204,7 +244,6 @@ const testsForConfigs: HintTest[] = [
content: htmlPage,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-Frame-Options': 'SAMEORIGIN',
'X-Test-1': 'test',
'X-Test-2': 'test'
}
Expand All @@ -223,11 +262,11 @@ const testsForConfigs: HintTest[] = [
];

testHint(hintPath, testsForDefaults);
testHint(hintPath, testsForIgnoreConfigs, { hintOptions: { ignore: ['Content-Security-Policy', 'X-UA-Compatible', 'X-Test-1'] } });
testHint(hintPath, testsForIncludeConfigs, { hintOptions: { include: ['Content-Security-Policy', 'X-Test-1', 'X-Test-2'] } });
testHint(hintPath, testsForIgnoreConfigs, { hintOptions: { ignore: ['X-UA-Compatible', 'X-Test-1'] } });
testHint(hintPath, testsForIncludeConfigs, { hintOptions: { include: ['X-Test-1', 'X-Test-2'] } });
testHint(hintPath, testsForConfigs, {
hintOptions: {
ignore: ['X-Frame-Options', 'X-Test-2', 'X-Test-3'],
ignore: ['X-Test-2', 'X-Test-3'],
include: ['X-Test-1', 'X-Test-2', 'X-UA-Compatible']
}
});

0 comments on commit fbb8ff3

Please sign in to comment.