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

Breaking: Update hint to check for XCTO header on all resources #1842

Merged
merged 4 commits into from
Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
87 changes: 75 additions & 12 deletions packages/hint-x-content-type-options/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Use `X-Content-Type-Options` header (`x-content-type-options`)

`x-content-type-options` requires that all scripts and
stylesheets are served with the `X-Content-Type-Options: nosniff`
`x-content-type-options` requires that all resources are
served with the `X-Content-Type-Options: nosniff`
HTTP response header.

## Why is this important?
Expand Down Expand Up @@ -29,19 +29,19 @@ header is sent for the script and the browser detects that it’s a script
and it wasn’t served with one of the [JavaScript media types][javascript
media types], the script will be blocked.

Note: [Modern browsers only respect the header for scripts and
stylesheets][fetch spec blocking] and sending the header for other
resources (such as images) when they are served with the wrong media
type may [create problems in older browsers][fetch spec issue].
While [modern browsers respect the header mainly for scripts and
stylesheets][fetch spec blocking], [Chromium uses this response header on
other resources][chromium ssca] for
[Cross-Origin Read Blocking][chromium corb].

## What does the hint check?

The hint checks if all scripts and stylesheets are served with the
The hint checks if all resources are served with the
`X-Content-Type-Options` HTTP headers with the value of `nosniff`.

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

Resource that is not script or stylesheet is served with the
Resource is not served with the
`X-Content-Type-Options` HTTP header.

```text
Expand All @@ -50,7 +50,6 @@ HTTP/... 200 OK
...

Content-Type: image/png
X-Content-Type-Options: nosniff
```

Script is served with the `X-Content-Type-Options` HTTP header
Expand All @@ -77,6 +76,69 @@ Content-Type: text/javascript; charset=utf-8
X-Content-Type-Options: nosniff
```

## How to configure the server to pass this hint

<details><summary>How to configure Apache</summary>

Apache can be configured to add headers using the [`Header`
directive][header directive].

```apache
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
</IfModule>
```

Note that:

* The above snippet works with Apache `v2.2.0+`, but you need to have
[`mod_headers`][mod_headers] [enabled][how to enable apache modules]
for it to take effect.

* If you have access to the [main Apache configuration file][main
apache conf file] (usually called `httpd.conf`), you should add
the logic in, for example, a [`<Directory>`][apache directory]
section in that file. This is usually the recommended way as
[using `.htaccess` files slows down][htaccess is slow] Apache!

If you don't have access to the main configuration file (quite
common with hosting services), add the snippets in a `.htaccess`
file in the root of the web site/app.

For the complete set of configurations, not just for this rule, see
the [Apache server configuration related documentation][apache config].

</details>

<details>

<summary>How to configure IIS</summary>

You can add this header unconditionally to all responses.

```xml
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
```

Note that:

* The above snippet works with IIS 7+.
* You should use the above snippet in the `web.config` of your
application.

For the complete set of configurations, not just for this rule,
see the [IIS server configuration related documentation][iis config].

</details>

## How to use this hint?

To use it you will have to install it via `npm`:
Expand Down Expand Up @@ -116,6 +178,8 @@ And then activate it via the [`.hintrc`][hintrc] configuration file:
<!-- Link labels: -->

[fetch spec blocking]: https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff%3F
[chromium ssca]: https://www.chromium.org/Home/chromium-security/ssca
[chromium corb]: https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md
[fetch spec issue]: https://github.com/whatwg/fetch/issues/395
[javascript media types]: https://html.spec.whatwg.org/multipage/scripting.html#javascript-mime-type
[mime sniffing spec]: https://mimesniff.spec.whatwg.org/
Expand All @@ -126,11 +190,10 @@ And then activate it via the [`.hintrc`][hintrc] configuration file:
[apache directory]: https://httpd.apache.org/docs/current/mod/core.html#directory
[header directive]: https://httpd.apache.org/docs/current/mod/mod_headers.html#header
[how to enable apache modules]: https://github.com/h5bp/server-configs-apache/tree/7eb30da6a06ec4fc24daf33c75b7bd86f9ad1f68#enable-apache-httpd-modules
[htaccess is slow]: https://httpd.apache.org/docs/current/howto/htaccess.html#when
[main apache conf file]: https://httpd.apache.org/docs/current/configuring.html#main
[htaccess is slow]: https://httpd.apache.org/docs/current/howto/htaccess.html#when
[mod_headers]: https://httpd.apache.org/docs/current/mod/mod_headers.html
[mod_mime]: https://httpd.apache.org/docs/current/mod/mod_mime.html

<!-- IIS links -->

[url rewrite]: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-the-url-rewrite-module
[iis config]: https://webhint.io/docs/user-guide/server-configurations/iis/
52 changes: 8 additions & 44 deletions packages/hint-x-content-type-options/src/hint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { debug as d } from 'hint/dist/src/lib/utils/debug';
import { IAsyncHTMLElement, FetchEnd, IHint } from 'hint/dist/src/lib/types';
import { FetchEnd, IHint } from 'hint/dist/src/lib/types';
import normalizeString from 'hint/dist/src/lib/utils/misc/normalize-string';
import isDataURI from 'hint/dist/src/lib/utils/network/is-data-uri';
import { HintContext } from 'hint/dist/src/lib/hint-context';
Expand All @@ -28,36 +28,6 @@ export default class XContentTypeOptionsHint implements IHint {

public static readonly meta = meta;
public constructor(context: HintContext) {

const isHeaderRequired = (element: IAsyncHTMLElement | null): boolean => {
if (!element) {
return false;
}

const nodeName = normalizeString(element.nodeName);

/*
* See:
*
* * https://github.com/whatwg/fetch/issues/395
* * https://fetch.spec.whatwg.org/#x-content-type-options-header
*/

if (nodeName === 'script') {
return true;

}

if (nodeName === 'link') {
// We check if element exists before and `normalizeString` will return `''` as default
const relValues = (normalizeString(element.getAttribute('rel'), ''))!.split(' ');

return relValues.includes('stylesheet');
}

return false;
};

const validate = async ({ element, resource, response }: FetchEnd) => {
// This check does not make sense for data URI.

Expand All @@ -69,25 +39,19 @@ export default class XContentTypeOptionsHint implements IHint {

const headerValue: string | null = normalizeString(response.headers && response.headers['x-content-type-options']);

if (isHeaderRequired(element)) {
if (headerValue === null) {
await context.report(resource, `Response should include 'x-content-type-options' header.`, { element });
if (headerValue === null) {
await context.report(resource, `Response should include 'x-content-type-options' header.`, { element });

return;
}

if (headerValue !== 'nosniff') {
await context.report(resource, `'x-content-type-options' header value should be 'nosniff', not '${headerValue}'.`, { element });
return;
}

return;
}
if (headerValue !== 'nosniff') {
await context.report(resource, `'x-content-type-options' header value should be 'nosniff', not '${headerValue}'.`, { element });

return;
}

if (headerValue) {
await context.report(resource, `Response should not include unneeded 'x-content-type-options' header.`, { element });
}
return;
};

context.on('fetch::end::*', validate);
Expand Down
45 changes: 28 additions & 17 deletions packages/hint-x-content-type-options/tests/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import { getHintPath } from 'hint/dist/src/lib/utils/hint-helpers';
import { HintTest } from '@hint/utils-tests-helpers/dist/src/hint-test-type';
import * as hintRunner from '@hint/utils-tests-helpers/dist/src/hint-runner';

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Page data.

const pageWithAlternateStylesheet = generateHTMLPage('<link rel=" alternate stylesheet " href="test.css">');
const pageWithScript = generateHTMLPage(undefined, '<script src="test.js"></script>');
const pageWithScriptAndStylesheet = generateHTMLPage('<link rel="stylesheet" href="test.css">', '<script src="test.js"></script>');
const pageWithStylesheet = generateHTMLPage('<link rel="stylesheet" href="test.css">');

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// Error messages.

const noHeaderErrorMessage = `Response should include 'x-content-type-options' header.`;
const unneededHeaderErrorMessage = `Response should not include unneeded 'x-content-type-options' header.`;

const generateInvalidValueMessage = (value: string = '') => {
return `'x-content-type-options' header value should be 'nosniff', not '${value}'.`;
};
Expand All @@ -27,48 +26,60 @@ const generateInvalidValueMessage = (value: string = '') => {
const tests: HintTest[] = [
{
name: `HTML page is served without 'X-Content-Type-Options' header`,
serverConfig: { '/': '' }
reports: [{ message: noHeaderErrorMessage }],
serverConfig: {
'/': { content: generateHTMLPage() },
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } }
}
},
{
name: `Script is served without 'X-Content-Type-Options' header`,
name: `Favicon is served without 'X-Content-Type-Options' header`,
reports: [{ message: noHeaderErrorMessage }],
serverConfig: {
'/': pageWithScript,
'/test.js': ''
'/': { content: generateHTMLPage(), headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
'/favicon.ico': ''
}
},
{
name: `Stylesheet is served without 'X-Content-Type-Options' header`,
reports: [{ message: noHeaderErrorMessage }],
serverConfig: {
'/': pageWithStylesheet,
'/': { content: pageWithStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } },
'/test.css': ''
}
},
{
name: `Alternate stylesheet is served without 'X-Content-Type-Options' header`,
reports: [{ message: noHeaderErrorMessage }],
serverConfig: {
'/': pageWithAlternateStylesheet,
'/': { content: pageWithAlternateStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } },
'/test.css': ''
}
},
{
name: `Resource is specified as a data URI`,
serverConfig: { '/': generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">') }
},
{
name: `HTML page is served with the 'X-Content-Type-Options' header`,
reports: [{ message: unneededHeaderErrorMessage }],
serverConfig: { '/': { headers: { 'X-Content-Type-Options': 'nosniff' } } }
serverConfig: {
'/': {
content: generateHTMLPage(undefined, '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">'),
headers: { 'X-Content-Type-Options': 'nosniff' }
},
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } }
}
},
{
name: `Script is served with 'X-Content-Type-Options' header with invalid value`,
reports: [{ message: generateInvalidValueMessage('invalid') }],
serverConfig: {
'/': pageWithScript,
'/': { content: pageWithScript, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } },
'/favicon.ico': { headers: { 'X-Content-Type-Options': 'nosniff' } },
'/test.js': { headers: { 'X-Content-Type-Options': 'invalid' } }
}
},
{
name: `All resources are served with 'X-Content-Type-Options' header`,
serverConfig: { '*': { content: pageWithScriptAndStylesheet, headers: { 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' } } }
}
];

Expand Down
11 changes: 11 additions & 0 deletions packages/hint/docs/user-guide/server-configurations/apache.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,17 @@ AddDefaultCharset utf-8
# Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# </IfModule>

# ----------------------------------------------------------------------
# | X-Content-Type-Options |
# ----------------------------------------------------------------------

# Serve resources with the x-content-type-options header set to `nosniff`.
# https://webhint.io/docs/user-guide/hints/hint-x-content-type-options/

# <IfModule mod_headers.c>
# Header always set X-Content-Type-Options nosniff
# </IfModule>


# ######################################################################
# # Unnedded / Disallowed headers #
Expand Down
15 changes: 6 additions & 9 deletions packages/hint/docs/user-guide/server-configurations/iis.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ related hint.
<remove name="X-Version"/>
<!-- Security headers ("strict-transport-security") -->
<add name="Strict-Transport-Security" value="max-age=31536000"/>
<!--
Security headers ("x-content-type-options")
All resources must serve with this response header set to "nosniff"
https://webhint.io/docs/user-guide/hints/hint-x-content-type-options/
-->
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>
<!--
Expand Down Expand Up @@ -143,15 +149,6 @@ related hint.
<action type="Rewrite" value="{C:3}" />
</rule>

<!-- Remove X-Content-Type from everywhere but JS and CSS ("x-content-type-options") -->
<rule name="X-Content-Type-Options" enabled="true">
<match serverVariable="RESPONSE_X_Content_Type_Options" pattern=".*" />
<conditions>
<add input="{RESPONSE_Content_Type}" pattern="text/(javascript|css)" />
</conditions>
<action type="Rewrite" value="nosniff"/>
</rule>

<!--
Add vary header
"http-compression": https://webhint.io/docs/user-guide/hints/hint-http-compression
Expand Down