Skip to content

Commit

Permalink
New: Make no-disallowed-headers allow Server
Browse files Browse the repository at this point in the history
For some servers such as Apache¹, the `Server` header cannot be removed
without, for example, installing an external module².

So, in order to better reflect reality, change `no-disallowed-headers`
rule to allow by default the `Server` header, however limit what
information its value can contain (namely, try to only allow the server
name³).

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

¹ https://bz.apache.org/bugzilla/show_bug.cgi?id=40026
² https://superuser.com/a/286825
³ https://httpd.apache.org/docs/current/mod/core.html#servertokens

Fix webhintio#747
Close webhintio#759
  • Loading branch information
alrra committed Jan 16, 2018
1 parent 32474f6 commit 188d270
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 14 deletions.
12 changes: 11 additions & 1 deletion docs/user-guide/rules/no-disallowed-headers.md
Expand Up @@ -32,13 +32,15 @@ HTTP headers:

* `Public-Key-Pins`
* `Public-Key-Pins-Report-Only`
* `Server`
* `X-AspNet-Version`
* `X-AspNetMvc-version`
* `X-Powered-By`
* `X-Runtime`
* `X-Version`

or the `Server` header with a value that provides a lot of information,
and is not limited to the server name.

### Examples that **trigger** the rule

```text
Expand All @@ -65,6 +67,14 @@ Public-Key-Pins-Report-Only:
```text
HTTP/... 200 OK
...
Server: apache
X-Powered-By: PHP/5.3.28
```

```text
HTTP/... 200 OK
...
```

Expand Down
90 changes: 83 additions & 7 deletions src/lib/rules/no-disallowed-headers/no-disallowed-headers.ts
Expand Up @@ -12,9 +12,10 @@ import * as pluralize from 'pluralize';

import { Category } from '../../enums/category';
import { debug as d } from '../../utils/debug';
import { getIncludedHeaders, mergeIgnoreIncludeArrays } from '../../utils/rule-helpers';
import { getIncludedHeaders, mergeIgnoreIncludeArrays, toLowerCase } from '../../utils/rule-helpers';
import { IAsyncHTMLElement, IFetchEnd, IRule, IRuleBuilder } from '../../types';
import { isDataURI } from '../../utils/misc';
import { IResponse } from '../../types/network';
import { getHeaderValueNormalized, isDataURI } from '../../utils/misc';
import { RuleContext } from '../../rule-context';

const debug = d(__filename);
Expand All @@ -31,23 +32,72 @@ const rule: IRuleBuilder = {
let disallowedHeaders: Array<string> = [
'public-key-pins',
'public-key-pins-report-only',
'server',
'x-aspnet-version',
'x-aspnetmvc-version',
'x-powered-by',
'x-runtime',
'x-version'
];

let includeHeaders;
let ignoreHeaders;

const loadRuleConfigs = () => {
const includeHeaders = (context.ruleOptions && context.ruleOptions.include) || [];
const ignoreHeaders = (context.ruleOptions && context.ruleOptions.ignore) || [];
includeHeaders = (context.ruleOptions && context.ruleOptions.include) || [];
ignoreHeaders = (context.ruleOptions && context.ruleOptions.ignore) || [];

disallowedHeaders = mergeIgnoreIncludeArrays(disallowedHeaders, ignoreHeaders, includeHeaders);
};

const serverHeaderContainsTooMuchInformation = (serverHeaderValue: string): boolean => {

const regex = [
/*
* Version numbers.
*
* e.g.:
*
* apache/2.2.24 (unix) mod_ssl/2.2.24 openssl/1.0.1e-fips mod_fastcgi/2.4.6
* marrakesh 1.9.9
* omniture dc/2.0.0
* microsoft-iis/8.5
* pingmatch/v2.0.30-165-g51bed16#rel-ec2-master i-077d449239c04b184@us-west-2b@dxedge-app_us-west-2_prod_asg
*/

/\/?v?\d\.(\d+\.?)*/,

/*
* OS/platforms names (usually enclose between parentheses).
*
* e.g.:
*
* apache/2.2.24 (unix) mod_ssl/2.2.24 openssl/1.0.1e-fips mod_fastcgi/2.4.6
* apache/2.2.34 (amazon)
* nginx/1.4.6 (ubuntu)
*/

/\(.*\)/,

/*
* Compiled-in modules.
*
* e.g.:
*
* apache/2.2.24 (unix) mod_ssl/2.2.24 openssl/1.0.1e-fips mod_fastcgi/2.4.6
* apache/2.4.6 (centos) php/5.4.16
* jino.ru/mod_pizza
*/

/(mod_|openssl|php)/
];

return regex.some((r) => {
return r.test(serverHeaderValue);
});
};

const validate = async (fetchEnd: IFetchEnd) => {
const { element, resource }: {element: IAsyncHTMLElement, resource: string} = fetchEnd;
const { element, response, resource }: { element: IAsyncHTMLElement, response: IResponse, resource: string } = fetchEnd;

// This check does not make sense for data URI.

Expand All @@ -57,9 +107,35 @@ const rule: IRuleBuilder = {
return;
}

const headers: Array<string> = getIncludedHeaders(fetchEnd.response.headers, disallowedHeaders);
const headers: Array<string> = getIncludedHeaders(response.headers, disallowedHeaders);
const numberOfHeaders: number = headers.length;

/*
* If the response contains the `server` header, and
* `server` is not specified by the user as a disallowed
* header or a header to be ignored, check if it provides
* more information than it should.
*
* The `Server` header is treated differently than the
* other ones because it cannot always be remove. In some
* cases such as Apache the best that the user can do is
* limit it's value to the name of the server (i.e. apache).
*
* See also:
*
* * https://bz.apache.org/bugzilla/show_bug.cgi?id=40026
* * https://httpd.apache.org/docs/current/mod/core.html#servertokens
*/

const serverHeaderValue = getHeaderValueNormalized(response.headers, 'server');

if (!disallowedHeaders.includes('server') &&
!toLowerCase(ignoreHeaders).includes('server') &&
serverHeaderValue &&
serverHeaderContainsTooMuchInformation(serverHeaderValue)) {
await context.report(resource, element, `'Server' header value contains more than the server name`);
}

if (numberOfHeaders > 0) {
await context.report(resource, element, `'${headers.join('\', \'')}' ${pluralize('header', numberOfHeaders)} ${pluralize('is', numberOfHeaders)} disallowed`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils/rule-helpers.ts
@@ -1,7 +1,7 @@
import * as path from 'path';

/** Lower cases all the items of `list`. */
const toLowerCase = (list: Array<string>): Array<string> => {
export const toLowerCase = (list: Array<string>): Array<string> => {
return list.map((e) => {
return e.toLowerCase();
});
Expand Down
76 changes: 71 additions & 5 deletions tests/lib/rules/no-disallowed-headers/tests.ts
Expand Up @@ -46,25 +46,90 @@ const testsForDefaults: Array<IRuleTest> = [
},
{
name: `HTML page is served with multiple disallowed headers`,
reports: [{ message: generateMessage(['server', 'x-aspnetmvc-version']) }],
reports: [{ message: generateMessage(['x-aspnetmvc-version', 'x-powered-by']) }],
serverConfig: {
'/': {
headers: {
Server: 'test',
'X-AspNetMvc-Version': 'test'
'X-AspNetMvc-Version': 'test',
'X-Powered-By': 'test'
}
}
}
}
];

const testsForDifferentServerHeaderValues: Array<IRuleTest> = (() => {

const allowedServerHeaderValues = [
'amo-cookiemap',
'aorta',
'APACHE',
'ecs',
'jetty',
'jino.ru',
'lighttpd',
'marrakesh',
'microsoft-iis',
'mt3',
'nginx',
'omniture',
'pingmatch',
'radiumone',
'waf',
'windows-azure-blo'
];

const disallowedServerHeaderValues = [
'Apache/2.2.24 (uNix) Mod_ssl/2.2.24 OpenSSl/1.0.1e-fips MOD_fastcgi/2.4.6',
'jetty(9.4.6.v20170531)',
'windows-azure-blob/1.0 microsoft-httpapi/2.0',
'apache/2.4.6 (CENTOS) PHP/5.4.16',
'apache/2.2.34 (amazon)',
'omniture dc/2.0.0',
'jino.ru/mod_pizza',
'amo-cookiemap/1.1',
'lighttpd/1.4.35',
'radiumone/1.4.2',
'mt3 1.15.20.1 33bcb65 release pao-pixel-x16',
'aorta/2.4.13-20180105.e4d0482',
'marrakesh 1.9.9',
'waf/2.4-12.1',
'ecs (sjc/4e6a)',
'pingmatch/v2.0.30-165-g51bed16#rel-ec2-master i-077d449239c04b184@us-west-2b@dxedge-app_us-west-2_prod_asg',
'microsoft-iis/8.5',
'nginx/1.12.2',
'NgiNx/1.4.6 (ubuntu)'
];

const tests = [];

allowedServerHeaderValues.forEach((value) => {
tests.push({
name: `HTML page is served with allowed 'Server: ${value}'`,
serverConfig: { '/': { headers: { Server: value } } }
});
});

disallowedServerHeaderValues.forEach((value) => {
tests.push({
name: `HTML page is served with disallowed 'Server: ${value}'`,
reports: [{ message: `'Server' header value contains more than the server name` }],
serverConfig: { '/': { headers: { Server: value } } }
});
});

return tests;

})();

const testsForIgnoreConfigs: Array<IRuleTest> = [
{
name: `HTML page is served with disallowed headers that are ignored because of configs`,
serverConfig: {
'/': {
headers: {
Server: 'test',
Server: 'apache/2.2.24 (unix) mod_ssl/2.2.24 openssl/1.0.1e-fips mod_fastcgi/2.4.6',
'X-Test-1': 'test'
}
}
Expand All @@ -80,7 +145,7 @@ const testsForIncludeConfigs: Array<IRuleTest> = [
'/': htmlPageWithScript,
'/test.js': {
headers: {
Server: 'test',
Server: 'apache/2.2.24 (unix) mod_ssl/2.2.24 openssl/1.0.1e-fips mod_fastcgi/2.4.6',
'X-Test-2': 'test'
}
}
Expand All @@ -95,7 +160,7 @@ const testsForConfigs: Array<IRuleTest> = [
serverConfig: {
'/': {
headers: {
Server: 'test',
Server: 'apache/2.2.24 (unix) mod_ssl/2.2.24 openssl/1.0.1e-fips mod_fastcgi/2.4.6',
'X-Powered-By': 'test',
'X-Test-1': 'test',
'X-Test-2': 'test'
Expand All @@ -106,6 +171,7 @@ const testsForConfigs: Array<IRuleTest> = [
];

ruleRunner.testRule(ruleName, testsForDefaults);
ruleRunner.testRule(ruleName, testsForDifferentServerHeaderValues);
ruleRunner.testRule(ruleName, testsForIgnoreConfigs, { ruleOptions: { ignore: ['Server', 'X-Powered-By', 'X-Test-1'] } });
ruleRunner.testRule(ruleName, testsForIncludeConfigs, { ruleOptions: { include: ['Server', 'X-Test-1', 'X-Test-2'] } });
ruleRunner.testRule(ruleName, testsForConfigs, {
Expand Down

0 comments on commit 188d270

Please sign in to comment.