Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions x-pack/dev-tools/jest/setup/polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ const MutationObserver = require('mutation-observer');
Object.defineProperty(window, 'MutationObserver', { value: MutationObserver });

require('whatwg-fetch');
const URL = { createObjectURL: () => '' };
Object.defineProperty(window, 'URL', { value: URL });

if (!global.URL.hasOwnProperty('createObjectURL')) {
Object.defineProperty(global.URL, 'createObjectURL', { value: () => '' });
}
88 changes: 88 additions & 0 deletions x-pack/plugins/security/common/is_internal_url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { isInternalURL } from './is_internal_url';

describe('isInternalURL', () => {
describe('with basePath defined', () => {
const basePath = '/iqf';

it('should return `true `if URL includes hash fragment', () => {
const href = `${basePath}/app/kibana#/discover/New-Saved-Search`;
expect(isInternalURL(href, basePath)).toBe(true);
});

it('should return `false` if URL includes a protocol/hostname', () => {
const href = `https://example.com${basePath}/app/kibana`;
expect(isInternalURL(href, basePath)).toBe(false);
});

it('should return `false` if URL includes a port', () => {
const href = `http://localhost:5601${basePath}/app/kibana`;
expect(isInternalURL(href, basePath)).toBe(false);
});

it('should return `false` if URL does not specify protocol', () => {
const hrefWithTwoSlashes = `/${basePath}/app/kibana`;
expect(isInternalURL(hrefWithTwoSlashes)).toBe(false);

const hrefWithThreeSlashes = `//${basePath}/app/kibana`;
expect(isInternalURL(hrefWithThreeSlashes)).toBe(false);
});

it('should return `true` if URL starts with a basepath', () => {
for (const href of [basePath, `${basePath}/`, `${basePath}/login`, `${basePath}/login/`]) {
expect(isInternalURL(href, basePath)).toBe(true);
}
});

it('should return `false` if URL does not start with basePath', () => {
for (const href of [
'/notbasepath/app/kibana',
`${basePath}_/login`,
basePath.slice(1),
`${basePath.slice(1)}/app/kibana`,
]) {
expect(isInternalURL(href, basePath)).toBe(false);
}
});

it('should return `true` if relative path does not escape base path', () => {
const href = `${basePath}/app/kibana/../../management`;
expect(isInternalURL(href, basePath)).toBe(true);
});

it('should return `false` if relative path escapes base path', () => {
const href = `${basePath}/app/kibana/../../../management`;
expect(isInternalURL(href, basePath)).toBe(false);
});
});

describe('without basePath defined', () => {
it('should return `true `if URL includes hash fragment', () => {
const href = '/app/kibana#/discover/New-Saved-Search';
expect(isInternalURL(href)).toBe(true);
});

it('should return `false` if URL includes a protocol/hostname', () => {
const href = 'https://example.com/app/kibana';
expect(isInternalURL(href)).toBe(false);
});

it('should return `false` if URL includes a port', () => {
const href = 'http://localhost:5601/app/kibana';
expect(isInternalURL(href)).toBe(false);
});

it('should return `false` if URL does not specify protocol', () => {
const hrefWithTwoSlashes = `//app/kibana`;
expect(isInternalURL(hrefWithTwoSlashes)).toBe(false);

const hrefWithThreeSlashes = `///app/kibana`;
expect(isInternalURL(hrefWithThreeSlashes)).toBe(false);
});
});
});
38 changes: 38 additions & 0 deletions x-pack/plugins/security/common/is_internal_url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { parse } from 'url';

export function isInternalURL(url: string, basePath = '') {
const { protocol, hostname, port, pathname } = parse(
url,
false /* parseQueryString */,
true /* slashesDenoteHost */
);

// We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not
// detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but
// browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser
// hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`)
// and the first slash that belongs to path.
if (protocol !== null || hostname !== null || port !== null) {
return false;
}

if (basePath) {
// Now we need to normalize URL to make sure any relative path segments (`..`) cannot escape expected
// base path. We can rely on `URL` with a localhost to automatically "normalize" the URL.
const normalizedPathname = new URL(String(pathname), 'https://localhost').pathname;
return (
// Normalized pathname can add a leading slash, but we should also make sure it's included in
// the original URL too
pathname?.startsWith('/') &&
(normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`))
);
}

return true;
}
20 changes: 3 additions & 17 deletions x-pack/plugins/security/common/parse_next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { parse } from 'url';
import { isInternalURL } from './is_internal_url';

export function parseNext(href: string, basePath = '') {
const { query, hash } = parse(href, true);
Expand All @@ -20,23 +21,8 @@ export function parseNext(href: string, basePath = '') {
}

// validate that `next` is not attempting a redirect to somewhere
// outside of this Kibana install
const { protocol, hostname, port, pathname } = parse(
next,
false /* parseQueryString */,
true /* slashesDenoteHost */
);

// We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not
// detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but
// browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser
// hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`)
// and the first slash that belongs to path.
if (protocol !== null || hostname !== null || port !== null) {
return `${basePath}/`;
}

if (!String(pathname).startsWith(basePath)) {
// outside of this Kibana install.
if (!isInternalURL(next, basePath)) {
return `${basePath}/`;
}

Expand Down
Loading