Skip to content

Commit 8548dbe

Browse files
authored
[7.x] Support deep links inside of RelayState for SAML IdP initiated login. (#69773)
# Conflicts: # x-pack/plugins/security/server/authentication/providers/saml.test.ts # x-pack/plugins/security/server/authentication/providers/saml.ts # x-pack/plugins/security/server/routes/authentication/saml.ts * Fix eslint warning.
1 parent 30b997f commit 8548dbe

12 files changed

Lines changed: 606 additions & 34 deletions

File tree

x-pack/dev-tools/jest/setup/polyfills.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ const MutationObserver = require('mutation-observer');
1717
Object.defineProperty(window, 'MutationObserver', { value: MutationObserver });
1818

1919
require('whatwg-fetch');
20-
const URL = { createObjectURL: () => '' };
21-
Object.defineProperty(window, 'URL', { value: URL });
20+
21+
if (!global.URL.hasOwnProperty('createObjectURL')) {
22+
Object.defineProperty(global.URL, 'createObjectURL', { value: () => '' });
23+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { isInternalURL } from './is_internal_url';
8+
9+
describe('isInternalURL', () => {
10+
describe('with basePath defined', () => {
11+
const basePath = '/iqf';
12+
13+
it('should return `true `if URL includes hash fragment', () => {
14+
const href = `${basePath}/app/kibana#/discover/New-Saved-Search`;
15+
expect(isInternalURL(href, basePath)).toBe(true);
16+
});
17+
18+
it('should return `false` if URL includes a protocol/hostname', () => {
19+
const href = `https://example.com${basePath}/app/kibana`;
20+
expect(isInternalURL(href, basePath)).toBe(false);
21+
});
22+
23+
it('should return `false` if URL includes a port', () => {
24+
const href = `http://localhost:5601${basePath}/app/kibana`;
25+
expect(isInternalURL(href, basePath)).toBe(false);
26+
});
27+
28+
it('should return `false` if URL does not specify protocol', () => {
29+
const hrefWithTwoSlashes = `/${basePath}/app/kibana`;
30+
expect(isInternalURL(hrefWithTwoSlashes)).toBe(false);
31+
32+
const hrefWithThreeSlashes = `//${basePath}/app/kibana`;
33+
expect(isInternalURL(hrefWithThreeSlashes)).toBe(false);
34+
});
35+
36+
it('should return `true` if URL starts with a basepath', () => {
37+
for (const href of [basePath, `${basePath}/`, `${basePath}/login`, `${basePath}/login/`]) {
38+
expect(isInternalURL(href, basePath)).toBe(true);
39+
}
40+
});
41+
42+
it('should return `false` if URL does not start with basePath', () => {
43+
for (const href of [
44+
'/notbasepath/app/kibana',
45+
`${basePath}_/login`,
46+
basePath.slice(1),
47+
`${basePath.slice(1)}/app/kibana`,
48+
]) {
49+
expect(isInternalURL(href, basePath)).toBe(false);
50+
}
51+
});
52+
53+
it('should return `true` if relative path does not escape base path', () => {
54+
const href = `${basePath}/app/kibana/../../management`;
55+
expect(isInternalURL(href, basePath)).toBe(true);
56+
});
57+
58+
it('should return `false` if relative path escapes base path', () => {
59+
const href = `${basePath}/app/kibana/../../../management`;
60+
expect(isInternalURL(href, basePath)).toBe(false);
61+
});
62+
});
63+
64+
describe('without basePath defined', () => {
65+
it('should return `true `if URL includes hash fragment', () => {
66+
const href = '/app/kibana#/discover/New-Saved-Search';
67+
expect(isInternalURL(href)).toBe(true);
68+
});
69+
70+
it('should return `false` if URL includes a protocol/hostname', () => {
71+
const href = 'https://example.com/app/kibana';
72+
expect(isInternalURL(href)).toBe(false);
73+
});
74+
75+
it('should return `false` if URL includes a port', () => {
76+
const href = 'http://localhost:5601/app/kibana';
77+
expect(isInternalURL(href)).toBe(false);
78+
});
79+
80+
it('should return `false` if URL does not specify protocol', () => {
81+
const hrefWithTwoSlashes = `//app/kibana`;
82+
expect(isInternalURL(hrefWithTwoSlashes)).toBe(false);
83+
84+
const hrefWithThreeSlashes = `///app/kibana`;
85+
expect(isInternalURL(hrefWithThreeSlashes)).toBe(false);
86+
});
87+
});
88+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { parse } from 'url';
8+
9+
export function isInternalURL(url: string, basePath = '') {
10+
const { protocol, hostname, port, pathname } = parse(
11+
url,
12+
false /* parseQueryString */,
13+
true /* slashesDenoteHost */
14+
);
15+
16+
// We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not
17+
// detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but
18+
// browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser
19+
// hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`)
20+
// and the first slash that belongs to path.
21+
if (protocol !== null || hostname !== null || port !== null) {
22+
return false;
23+
}
24+
25+
if (basePath) {
26+
// Now we need to normalize URL to make sure any relative path segments (`..`) cannot escape expected
27+
// base path. We can rely on `URL` with a localhost to automatically "normalize" the URL.
28+
const normalizedPathname = new URL(String(pathname), 'https://localhost').pathname;
29+
return (
30+
// Normalized pathname can add a leading slash, but we should also make sure it's included in
31+
// the original URL too
32+
pathname?.startsWith('/') &&
33+
(normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`))
34+
);
35+
}
36+
37+
return true;
38+
}

x-pack/plugins/security/common/parse_next.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { parse } from 'url';
8+
import { isInternalURL } from './is_internal_url';
89

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

2223
// validate that `next` is not attempting a redirect to somewhere
23-
// outside of this Kibana install
24-
const { protocol, hostname, port, pathname } = parse(
25-
next,
26-
false /* parseQueryString */,
27-
true /* slashesDenoteHost */
28-
);
29-
30-
// We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not
31-
// detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but
32-
// browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser
33-
// hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`)
34-
// and the first slash that belongs to path.
35-
if (protocol !== null || hostname !== null || port !== null) {
36-
return `${basePath}/`;
37-
}
38-
39-
if (!String(pathname).startsWith(basePath)) {
24+
// outside of this Kibana install.
25+
if (!isInternalURL(next, basePath)) {
4026
return `${basePath}/`;
4127
}
4228

0 commit comments

Comments
 (0)