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

[Backport 2.x] Update nextUrl validation to incorporate serverBasePath #2049

Merged
merged 1 commit into from
Jul 23, 2024
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
13 changes: 12 additions & 1 deletion server/auth/types/basic/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { resolveTenant } from '../../../multitenancy/tenant_resolver';
import { encodeUriQuery } from '../../../../../../src/plugins/opensearch_dashboards_utils/common/url/encode_uri_query';
import { AuthType } from '../../../../common';
import { validateNextUrl } from '../../../utils/next_url';

export class BasicAuthRoutes {
constructor(
Expand All @@ -47,7 +48,17 @@ export class BasicAuthRoutes {
this.coreSetup.http.resources.register(
{
path: LOGIN_PAGE_URI,
validate: false,
validate: {
query: schema.object({
nextUrl: schema.maybe(
schema.string({
validate: (nexturl) => {
return validateNextUrl(nexturl, this.coreSetup.http.basePath.serverBasePath);
},
})
),
}),
},
options: {
authRequired: false,
},
Expand Down
8 changes: 6 additions & 2 deletions server/auth/types/openid/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ export class OpenIdAuthRoutes {
code: schema.maybe(schema.string()),
nextUrl: schema.maybe(
schema.string({
validate: validateNextUrl,
validate: (nexturl) => {
return validateNextUrl(nexturl, this.core.http.basePath.serverBasePath);
},
})
),
redirectHash: schema.maybe(schema.boolean()),
Expand Down Expand Up @@ -293,7 +295,9 @@ export class OpenIdAuthRoutes {
query: schema.object({
nextUrl: schema.maybe(
schema.string({
validate: validateNextUrl,
validate: (nexturl) => {
return validateNextUrl(nexturl, this.core.http.basePath.serverBasePath);
},
})
),
}),
Expand Down
4 changes: 3 additions & 1 deletion server/auth/types/proxy/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export class ProxyAuthRoutes {
query: schema.object({
nextUrl: schema.maybe(
schema.string({
validate: validateNextUrl,
validate: (nexturl) => {
return validateNextUrl(nexturl, this.coreSetup.http.basePath.serverBasePath);
},
})
),
}),
Expand Down
8 changes: 6 additions & 2 deletions server/auth/types/saml/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export class SamlAuthRoutes {
query: schema.object({
nextUrl: schema.maybe(
schema.string({
validate: validateNextUrl,
validate: (nexturl) => {
return validateNextUrl(nexturl, this.coreSetup.http.basePath.serverBasePath);
},
})
),
redirectHash: schema.string(),
Expand Down Expand Up @@ -270,7 +272,9 @@ export class SamlAuthRoutes {
query: schema.object({
nextUrl: schema.maybe(
schema.string({
validate: validateNextUrl,
validate: (nexturl) => {
return validateNextUrl(nexturl, this.coreSetup.http.basePath.serverBasePath);
},
})
),
}),
Expand Down
33 changes: 23 additions & 10 deletions server/utils/next_url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,48 +69,54 @@ describe('test composeNextUrlQueryParam', () => {
describe('test validateNextUrl', () => {
test('accept relative url', () => {
const url = '/relative/path';
expect(validateNextUrl(url)).toEqual(undefined);
expect(validateNextUrl(url, '')).toEqual(undefined);
});

test('accept relative url with # and query', () => {
const url = '/relative/path#hash?a=b';
expect(validateNextUrl(url)).toEqual(undefined);
expect(validateNextUrl(url, undefined)).toEqual(undefined);
});

test('reject url not start with /', () => {
const url = 'exmaple.com/relative/path';
expect(validateNextUrl(url)).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
expect(validateNextUrl(url, '')).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
});

test('reject absolute url', () => {
const url = 'https://exmaple.com/relative/path';
expect(validateNextUrl(url)).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
expect(validateNextUrl(url, '')).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
});

test('reject url starts with //', () => {
const url = '//exmaple.com/relative/path';
expect(validateNextUrl(url)).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
expect(validateNextUrl(url, '')).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
});

test('accpet url has @ in query parameters', () => {
const url = '/test/path?key=a@b&k2=v';
expect(validateNextUrl(url)).toEqual(undefined);
expect(validateNextUrl(url, '')).toEqual(undefined);
});

test('allow slash', () => {
const url = '/';
expect(validateNextUrl(url)).toEqual(undefined);
expect(validateNextUrl(url, '')).toEqual(undefined);
});

test('allow dashboard url', () => {
const url =
'/_plugin/opensearch-dashboards/app/opensearch-dashboards#dashbard/dashboard-id?_g=(param=a&p=b)';
expect(validateNextUrl(url)).toEqual(undefined);
expect(validateNextUrl(url, '')).toEqual(undefined);
});

test('allow basePath with numbers', () => {
const url = '/123/app/dashboards';
expect(validateNextUrl(url, '/123')).toEqual(undefined);
});

// test cases from https://pentester.land/cheatsheets/2018/11/02/open-redirect-cheatsheet.html
test('test list', () => {
const urlList = [
'/\t/example.com/',
'<>//Ⓛ𝐨𝗰�𝕝ⅆ𝓸ⓜₐℹⓃ。Pⓦ',
'//;@Ⓛ𝐨𝗰�𝕝ⅆ𝓸ⓜₐℹⓃ。Pⓦ',
'/////Ⓛ𝐨𝗰�𝕝ⅆ𝓸ⓜₐℹⓃ。Pⓦ/',
Expand Down Expand Up @@ -624,10 +630,17 @@ describe('test validateNextUrl', () => {
'//XY>.7d8T\\[email protected]+@Ⓛ𝐨𝗰�𝕝ⅆ𝓸ⓜₐℹⓃ。Pⓦ/',
'//XY>.7d8T\\[email protected]@google.com/',
'//XY>.7d8T\\[email protected][email protected]/',
'javascript://sub.domain.com/%0Aalert(1)',
'javascript://%250Aalert(1)',
'javascript://%250Aalert(1)//?1',
'javascript://%250A1?alert(1):0',
'%09Jav%09ascript:alert(document.domain)',
'javascript://%250Alert(document.location=document.cookie)',
'\\j\\av\\a\\s\\cr\\i\\pt\\:\\a\\l\\ert\\(1\\)',
];
for (const url in urlList) {
for (const url of urlList) {
if (url) {
expect(validateNextUrl(url)).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
expect(validateNextUrl(url, '')).toEqual(INVALID_NEXT_URL_PARAMETER_MESSAGE);
}
}
});
Expand Down
28 changes: 17 additions & 11 deletions server/utils/next_url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,31 @@ export const INVALID_NEXT_URL_PARAMETER_MESSAGE = 'Invalid nextUrl parameter.';
/**
* We require the nextUrl parameter to be an relative url.
*
* Here we leverage the normalizeUrl function. If the library can parse the url
* parameter, which means it is an absolute url, then we reject it. Otherwise, the
* library cannot parse the url, which means it is not an absolute url, we let to
* go through.
* Here we validate the nextUrl parameter by checking if it meets the following criteria:
* - nextUrl starts with the basePath (/ if no serverBasePath is set)
* - If nextUrl is longer than 2 chars then the second character must be alphabetical or underscore
* - The following characters must be alphanumeric, dash or underscore
* Note: url has been decoded by OpenSearchDashboards.
*
* @param url url string.
* @returns error message if nextUrl is invalid, otherwise void.
*/
export const validateNextUrl = (url: string | undefined): string | void => {
export function validateNextUrl(
url: string | undefined,
basePath: string | undefined
): string | void {
if (url) {
const path = url.split('?')[0];
const path = url.split(/\?|#/)[0];
const bp = basePath || '';
if (!path.startsWith(bp)) {
return INVALID_NEXT_URL_PARAMETER_MESSAGE;
}
const pathMinusBase = path.replace(bp, '');
if (
!path.startsWith('/') ||
path.startsWith('//') ||
path.includes('\\') ||
path.includes('@')
!pathMinusBase.startsWith('/') ||
(pathMinusBase.length >= 2 && !/^\/[a-zA-Z_][\/a-zA-Z0-9-_]+$/.test(pathMinusBase))
) {
return INVALID_NEXT_URL_PARAMETER_MESSAGE;
}
}
};
}
Loading