Skip to content

Commit

Permalink
Hash configured hostnames before sending to client
Browse files Browse the repository at this point in the history
  • Loading branch information
legrego committed Dec 8, 2020
1 parent 0c0efe5 commit c1a9b13
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 10 deletions.
45 changes: 44 additions & 1 deletion src/core/public/http/external_url_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import { ExternalUrlConfig } from 'src/core/server/types';
import { createSHA256Hash } from '../../utils';

import { injectedMetadataServiceMock } from '../mocks';

Expand All @@ -32,8 +33,12 @@ const setupService = ({
serverBasePath: string;
policy: ExternalUrlConfig['policy'];
}) => {
const hashedPolicies = policy.map((entry) => ({
...entry,
host: entry.host ? createSHA256Hash(entry.host) : undefined,
}));
const injectedMetadata = injectedMetadataServiceMock.createSetupContract();
injectedMetadata.getExternalUrlConfig.mockReturnValue({ policy });
injectedMetadata.getExternalUrlConfig.mockReturnValue({ policy: hashedPolicies });
injectedMetadata.getServerBasePath.mockReturnValue(serverBasePath);

const service = new ExternalUrlService();
Expand Down Expand Up @@ -308,6 +313,44 @@ describe('External Url Service', () => {
expect(result?.toString()).toEqual(urlCandidate);
});

it('allows external urls with a partially matching host and protocol in the allow list', () => {
const { setup } = setupService({
location,
serverBasePath,
policy: [
{
allow: true,
host: 'google.com',
protocol: 'https',
},
],
});
const urlCandidate = `https://www.google.com/foo?bar=baz`;
const result = setup.validateUrl(urlCandidate);

expect(result).toBeInstanceOf(URL);
expect(result?.toString()).toEqual(urlCandidate);
});

it('allows external urls that specify a locally addressable host', () => {
const { setup } = setupService({
location,
serverBasePath,
policy: [
{
allow: true,
host: 'some-host-name',
protocol: 'https',
},
],
});
const urlCandidate = `https://some-host-name/foo?bar=baz`;
const result = setup.validateUrl(urlCandidate);

expect(result).toBeInstanceOf(URL);
expect(result?.toString()).toEqual(urlCandidate);
});

it('disallows external urls with a matching host and unmatched protocol', () => {
const { setup } = setupService({
location,
Expand Down
26 changes: 21 additions & 5 deletions src/core/public/http/external_url_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,35 @@
import { IExternalUrlPolicy } from 'src/core/server/types';

import { CoreService } from 'src/core/types';
import { InjectedMetadataSetup } from '../injected_metadata';
import { createSHA256Hash } from '../../utils';
import { IExternalUrl } from './types';
import { InjectedMetadataSetup } from '../injected_metadata';

interface SetupDeps {
location: Pick<Location, 'origin'>;
injectedMetadata: InjectedMetadataSetup;
}

const isHostMatch = (actualHost: string, ruleHost: string) => {
const hostParts = actualHost.split('.').reverse();
const ruleParts = ruleHost.split('.').reverse();
function* getHostHashes(actualHost: string) {
yield createSHA256Hash(actualHost);
let host = actualHost.substr(actualHost.indexOf('.') + 1);
while (host) {
const hash = createSHA256Hash(host);
yield hash;
if (host.indexOf('.') === -1) {
break;
}
host = host.substr(host.indexOf('.') + 1);
}
}

return ruleParts.every((part, idx) => part === hostParts[idx]);
const isHostMatch = (actualHost: string, ruleHostHash: string) => {
for (const hash of getHostHashes(actualHost)) {
if (hash === ruleHostHash) {
return true;
}
}
return false;
};

const isProtocolMatch = (actualProtocol: string, ruleProtocol: string) => {
Expand Down
11 changes: 10 additions & 1 deletion src/core/server/external_url/external_url_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { createSHA256Hash } from '../../utils';
import { config } from './config';

const DEFAULT_CONFIG = Object.freeze(config.schema.validate({}));
Expand Down Expand Up @@ -82,6 +83,14 @@ export class ExternalUrlConfig implements IExternalUrlConfig {
* @internal
*/
constructor(rawConfig: IExternalUrlConfig) {
this.policy = rawConfig.policy;
this.policy = rawConfig.policy.map((entry) => {
if (entry.host) {
return {
...entry,
host: createSHA256Hash(entry.host),
};
}
return entry;
});
}
}
2 changes: 1 addition & 1 deletion src/core/server/http/http_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export class HttpConfig {
this.ssl = new SslConfig(rawHttpConfig.ssl || {});
this.compression = rawHttpConfig.compression;
this.csp = new CspConfig(rawCspConfig);
this.externalUrl = new ExternalUrlConfig(rawExternalUrlConfig);
this.externalUrl = rawExternalUrlConfig;
this.xsrf = rawHttpConfig.xsrf;
this.requestId = rawHttpConfig.requestId;
}
Expand Down
8 changes: 6 additions & 2 deletions src/core/server/http/http_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ import {

import { RequestHandlerContext } from '../../server';
import { registerCoreHandlers } from './lifecycle_handlers';
import { ExternalUrlConfigType, config as externalUrlConfig } from '../external_url';
import {
ExternalUrlConfigType,
config as externalUrlConfig,
ExternalUrlConfig,
} from '../external_url';

interface SetupDeps {
context: ContextSetup;
Expand Down Expand Up @@ -105,7 +109,7 @@ export class HttpService
this.internalSetup = {
...serverContract,

externalUrl: config.externalUrl,
externalUrl: new ExternalUrlConfig(config.externalUrl),

createRouter: (path: string, pluginId: PluginOpaqueId = this.coreContext.coreId) => {
const enhanceHandler = this.requestHandlerContext!.createHandler.bind(null, pluginId);
Expand Down
39 changes: 39 additions & 0 deletions src/core/utils/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { createSHA256Hash } from './crypto';

describe('createSHA256Hash', () => {
it('creates a hex-encoded hash by default', () => {
expect(createSHA256Hash('foo')).toMatchInlineSnapshot(
`"2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"`
);
});

it('allows the output encoding to be changed', () => {
expect(createSHA256Hash('foo', 'base64')).toMatchInlineSnapshot(
`"LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564="`
);
});

it('accepts a buffer as input', () => {
const data = Buffer.from('foo', 'utf8');
expect(createSHA256Hash(data)).toEqual(createSHA256Hash('foo'));
});
});
33 changes: 33 additions & 0 deletions src/core/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import crypto, { HexBase64Latin1Encoding } from 'crypto';

export const createSHA256Hash = (
input: string | Buffer,
outputEncoding: HexBase64Latin1Encoding = 'hex'
) => {
let data: Buffer;
if (typeof input === 'string') {
data = Buffer.from(input, 'utf8');
} else {
data = input;
}
return crypto.createHash('sha256').update(data).digest(outputEncoding);
};
1 change: 1 addition & 0 deletions src/core/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export {
IContextContainer,
IContextProvider,
} from './context';
export { createSHA256Hash } from './crypto';
export { DEFAULT_APP_CATEGORIES } from './default_app_categories';

0 comments on commit c1a9b13

Please sign in to comment.