Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add rule to check for HTML documents only headers
- Loading branch information
Showing
8 changed files
with
472 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
src/lib/rules/no-html-only-headers/no-html-only-headers.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# Disallow unneeded HTTP headers for non-HTML resources (`no-html-only-headers`) | ||
|
||
`no-html-only-headers` warns against responding with HTTP headers that | ||
are not needed for non-HTML resources. | ||
|
||
|
||
## Why is this important? | ||
|
||
Some HTTP headers do not make sense to be send for non-HTML | ||
resources, as sending them does not provide any value to users, | ||
and just contributes to header bloat. | ||
|
||
|
||
## What does the rule check? | ||
|
||
The rule checks if non-HTML responses include any of the following | ||
HTTP headers: | ||
|
||
* `Content-Security-Policy` | ||
* `X-Content-Security-Policy` | ||
* `X-Frame-Options` | ||
* `X-UA-Compatible` | ||
* `X-WebKit-CSP` | ||
* `X-XSS-Protection` | ||
|
||
Examples that **trigger** the rule: | ||
|
||
Response for `/test.js`: | ||
|
||
```text | ||
HTTP/1.1 200 OK | ||
Content-Type: application/javascript | ||
... | ||
Content-Security-Policy: default-src 'none' | ||
Content-Type: application/javascript; 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 | ||
... | ||
``` | ||
|
||
Response for `/test.html`: | ||
|
||
```text | ||
HTTP/1.1 200 OK | ||
Content-Type: x/y | ||
... | ||
Content-Security-Policy: default-src 'none' | ||
Content-Type: application/javascript; 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 | ||
... | ||
``` | ||
|
||
Examples that **pass** the rule: | ||
|
||
Response for `/test.js`: | ||
|
||
```text | ||
HTTP/1.1 200 OK | ||
Content-Type: application/javascript | ||
... | ||
``` | ||
|
||
Response for `/test.html`: | ||
|
||
```text | ||
HTTP/1.1 200 OK | ||
Content-Type: text/html | ||
... | ||
Content-Security-Policy: default-src 'none' | ||
Content-Type: application/javascript; 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 | ||
... | ||
``` | ||
|
||
|
||
## Can the rule be configured? | ||
|
||
Yes, you can use: | ||
|
||
* `include` to specify additional HTTP headers that should | ||
be disallowed for non-HTML resources | ||
* `ignore` to specify which of the disallowed HTTP headers | ||
should be ignored | ||
|
||
E.g. The following configuration will make the rule allow non-HTML | ||
resources to be served with the `Content-Security-Policy` HTTP header, | ||
but not with `Custom-Header`. | ||
|
||
```json | ||
"no-html-only-headers": [ "warning", { | ||
"ignore": ["Content-Security-Policy"], | ||
"include": ["Custom-Header"] | ||
}] | ||
``` |
113 changes: 113 additions & 0 deletions
113
src/lib/rules/no-html-only-headers/no-html-only-headers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/** | ||
* @fileoverview Check if non HTML resources responses contain certain | ||
* unneeded HTTP headers. | ||
*/ | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Requirements | ||
// ------------------------------------------------------------------------------ | ||
|
||
import { IFetchEndEvent, IResponse, IRule, IRuleBuilder } from '../../interfaces'; // eslint-disable-line no-unused-vars | ||
import { RuleContext } from '../../rule-context'; // eslint-disable-line no-unused-vars | ||
import { getIncludedHeaders, mergeIgnoreIncludeArrays } from '../../util/rule-helpers'; | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Public | ||
// ------------------------------------------------------------------------------ | ||
|
||
const rule: IRuleBuilder = { | ||
create(context: RuleContext): IRule { | ||
|
||
let unneededHeaders = [ | ||
'content-security-policy', | ||
'x-content-security-policy', | ||
'x-frame-options', | ||
'x-ua-compatible', | ||
'x-webkit-csp', | ||
'x-xss-protection' | ||
]; | ||
|
||
const loadRuleConfigs = () => { | ||
const includeHeaders = (context.ruleOptions && context.ruleOptions.include) || []; | ||
const ignoreHeaders = (context.ruleOptions && context.ruleOptions.ignore) || []; | ||
|
||
unneededHeaders = mergeIgnoreIncludeArrays(unneededHeaders, ignoreHeaders, includeHeaders); | ||
}; | ||
|
||
const willBeTreatedAsHTML = (response: IResponse) => { | ||
const mediaType = response.headers['content-type'].split(';')[0].trim(); | ||
|
||
// By default, browsers will treat resource sent with the | ||
// following media types as HTML documents. | ||
|
||
if (['text/html', 'application/xhtml+xml'].includes(mediaType)) { | ||
return true; | ||
} | ||
|
||
// That is not the situation for other cases where the media | ||
// type is in the form of `<type>/<subtype>`. | ||
|
||
if (mediaType.indexOf('/') > 0) { | ||
return false; | ||
} | ||
|
||
// If the media type is not specified or invalid, browser | ||
// will try to sniff the content. | ||
// | ||
// https://mimesniff.spec.whatwg.org/ | ||
// | ||
// At this point, even if browsers may decide to treat | ||
// the content as a HTML document, things are obviously | ||
// not done correctly, so the decision was to not try to | ||
// also sniff the content, and instead, just signal this | ||
// as a problem. | ||
|
||
return false; | ||
}; | ||
|
||
const checkHeaders = (fetchEnd: IFetchEndEvent) => { | ||
const { element, resource, response } = fetchEnd; | ||
|
||
if (!willBeTreatedAsHTML(response)) { | ||
const headers = getIncludedHeaders(response.headers, unneededHeaders); | ||
|
||
if (headers.length > 0) { | ||
context.report(resource, element, `Unneeded HTTP header${headers.length > 1 ? 's' : ''} found: ${headers.join(', ')}`); | ||
} | ||
} | ||
}; | ||
|
||
loadRuleConfigs(); | ||
|
||
return { | ||
'fetch::end': checkHeaders, | ||
'targetfetch::end': checkHeaders | ||
}; | ||
}, | ||
meta: { | ||
docs: { | ||
category: 'performance', | ||
description: 'Disallow unneeded HTTP headers for non-HTML resources', | ||
recommended: true | ||
}, | ||
fixable: 'code', | ||
schema: { | ||
additionalProperties: false, | ||
definitions: { | ||
'string-array': { | ||
items: { type: 'string' }, | ||
minItems: 1, | ||
type: 'array', | ||
uniqueItems: true | ||
} | ||
}, | ||
properties: { | ||
ignore: { $ref: '#/definitions/string-array' }, | ||
include: { $ref: '#/definitions/string-array' } | ||
}, | ||
type: ['object', null] | ||
} | ||
} | ||
}; | ||
|
||
module.exports = rule; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,52 @@ | ||
import * as path from 'path'; | ||
import * as d from 'debug'; | ||
|
||
export const getIncludedHeaders = (headers: object = {}, headerList: Array<string> = []) => { | ||
const result = []; | ||
|
||
for (const [key] of Object.entries(headers)) { | ||
if (headerList.includes(key.toLowerCase())) { | ||
result.push(key); | ||
} | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
export const getRuleName = (dirname: string) => { | ||
return path.basename(dirname); | ||
}; | ||
|
||
export const mergeIgnoreIncludeArrays = (originalArray: Array<string> = [], ignoreArray: Array<string> = [], includeArray: Array<string> = []) => { | ||
|
||
let result = originalArray.map((e) => { | ||
return e.toLowerCase(); | ||
}); | ||
|
||
const include = includeArray.map((e) => { | ||
return e.toLowerCase(); | ||
}); | ||
|
||
const ignore = ignoreArray.map((e) => { | ||
return e.toLowerCase(); | ||
}); | ||
|
||
// Add elements specified under 'include'. | ||
include.forEach((e) => { | ||
if (!result.includes(e)) { | ||
result.push(e); | ||
} | ||
}); | ||
|
||
// Remove elements specified under 'ignore'. | ||
result = result.filter((e) => { | ||
return !ignore.includes(e); | ||
}); | ||
|
||
return result; | ||
|
||
}; | ||
|
||
export const ruleDebug = (dirname: string) => { | ||
return d(`sonar:rules:${getRuleName(dirname)}`); | ||
}; |
Oops, something went wrong.