Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix no html only headers #2618

Merged
merged 5 commits into from
Jun 21, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 30 additions & 20 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 @@ -99,12 +114,14 @@ you can do something such as the following:
# 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)$">
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|mjs|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|xml|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/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/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/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']
}
});