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

Update nextUrl validation to incorporate serverBasePath (#2048) (#2050) #2102

Closed
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
12 changes: 11 additions & 1 deletion server/auth/types/basic/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,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 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);

Check failure on line 108 in server/utils/next_url.test.ts

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

Delete `··`

Check failure on line 108 in server/utils/next_url.test.ts

View workflow job for this annotation

GitHub Actions / Run unit tests (windows-latest)

Delete `··`
});

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 @@
'//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 @@
/**
* 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:

Check failure on line 48 in server/utils/next_url.ts

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

Insert `·`

Check failure on line 48 in server/utils/next_url.ts

View workflow job for this annotation

GitHub Actions / Run unit tests (windows-latest)

Insert `·`
* - 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;
}
}
};
}

Check failure on line 75 in server/utils/next_url.ts

View workflow job for this annotation

GitHub Actions / Run unit tests (ubuntu-latest)

Insert `⏎`

Check failure on line 75 in server/utils/next_url.ts

View workflow job for this annotation

GitHub Actions / Run unit tests (windows-latest)

Insert `␍⏎`
Loading