Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions changelogs/fragments/10915.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- @osd-std package url parse method does not throw page crashing errors ([#10915](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10915))
22 changes: 16 additions & 6 deletions packages/osd-std/src/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@
import { modifyUrl, isRelativeUrl, getUrlOrigin } from './url';

describe('modifyUrl()', () => {
test('throws an error with invalid input', () => {
expect(() => modifyUrl(1 as any, () => ({}))).toThrowError();
expect(() => modifyUrl(undefined as any, () => ({}))).toThrowError();
expect(() => modifyUrl('http://localhost', undefined as any)).toThrowError();
});

test('supports returning a new url spec', () => {
expect(modifyUrl('http://localhost', () => ({}))).toEqual('');
});
Expand Down Expand Up @@ -79,6 +73,12 @@ describe('modifyUrl()', () => {
})
).toEqual('mail:localhost');
});

test('does not throw URIError for malformed urls', () => {
expect(() => {
modifyUrl('http://user:%E0%[email protected]', () => {});
}).not.toThrowError();
});
});

describe('isRelativeUrl()', () => {
Expand All @@ -93,6 +93,11 @@ describe('isRelativeUrl()', () => {
expect(isRelativeUrl('///evil.com')).toBe(false);
expect(isRelativeUrl(' //evil.com')).toBe(false);
});
test('does not throw URIError for malformed urls', () => {
expect(() => {
isRelativeUrl('http://user:%E0%[email protected]');
}).not.toThrowError();
});
});

describe('getOrigin', () => {
Expand All @@ -105,6 +110,11 @@ describe('getOrigin', () => {
it('return origin with port when the url does have a port', () => {
expect(getUrlOrigin('http://example.com:80/path/to/file')).toEqual('http://example.com:80');
});
it('does not throw URIError for malformed urls', () => {
expect(() => {
getUrlOrigin('http://user:%E0%[email protected]');
}).not.toThrowError();
});
});
describe('when passing a non absolute url', () => {
it('returns null for relative url', () => {
Expand Down
98 changes: 55 additions & 43 deletions packages/osd-std/src/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,36 +83,40 @@ export function modifyUrl(
url: string,
urlModifier: (urlParts: URLMeaningfulParts) => Partial<URLMeaningfulParts> | void
) {
const parsed = parseUrl(url, true) as URLMeaningfulParts;
try {
const parsed = parseUrl(url, true) as URLMeaningfulParts;

// Copy over the most specific version of each property. By default, the parsed url includes several
// conflicting properties (like path and pathname + search, or search and query) and keeping track
// of which property is actually used when they are formatted is harder than necessary.
const meaningfulParts: URLMeaningfulParts = {
auth: parsed.auth,
hash: parsed.hash,
hostname: parsed.hostname,
pathname: parsed.pathname,
port: parsed.port,
protocol: parsed.protocol,
query: parsed.query || {},
slashes: parsed.slashes,
};
// Copy over the most specific version of each property. By default, the parsed url includes several
// conflicting properties (like path and pathname + search, or search and query) and keeping track
// of which property is actually used when they are formatted is harder than necessary.
const meaningfulParts: URLMeaningfulParts = {
auth: parsed.auth,
hash: parsed.hash,
hostname: parsed.hostname,
pathname: parsed.pathname,
port: parsed.port,
protocol: parsed.protocol,
query: parsed.query || {},
slashes: parsed.slashes,
};

// The urlModifier modifies the meaningfulParts object, or returns a new one.
const modifiedParts = urlModifier(meaningfulParts) || meaningfulParts;
// The urlModifier modifies the meaningfulParts object, or returns a new one.
const modifiedParts = urlModifier(meaningfulParts) || meaningfulParts;

// Format the modified/replaced meaningfulParts back into a url.
return formatUrl({
auth: modifiedParts.auth,
hash: modifiedParts.hash,
hostname: modifiedParts.hostname,
pathname: modifiedParts.pathname,
port: modifiedParts.port,
protocol: modifiedParts.protocol,
query: modifiedParts.query,
slashes: modifiedParts.slashes,
} as UrlObject);
// Format the modified/replaced meaningfulParts back into a url.
return formatUrl({
auth: modifiedParts.auth,
hash: modifiedParts.hash,
hostname: modifiedParts.hostname,
pathname: modifiedParts.pathname,
port: modifiedParts.port,
protocol: modifiedParts.protocol,
query: modifiedParts.query,
slashes: modifiedParts.slashes,
} as UrlObject);
} catch (error) {
return url; // Safe fallback to original url
}
}

/**
Expand All @@ -122,28 +126,36 @@ export function modifyUrl(
* @public
*/
export function isRelativeUrl(candidatePath: string) {
// validate that `candidatePath` is not attempting a redirect to somewhere
// outside of this OpenSearch Dashboards install
const all = parseUrl(candidatePath, false /* parseQueryString */, true /* slashesDenoteHost */);
const { protocol, hostname, port } = all;
// 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;
try {
// validate that `candidatePath` is not attempting a redirect to somewhere
// outside of this OpenSearch Dashboards install
const all = parseUrl(candidatePath, false /* parseQueryString */, true /* slashesDenoteHost */);
const { protocol, hostname, port } = all;
// 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;
}
return true;
} catch (error) {
return false; // Safe fallback to "not relative"
}
return true;
}

/**
* Returns the origin (protocol + host + port) from given `url` if `url` is a valid absolute url, or null otherwise
*/
export function getUrlOrigin(url: string): string | null {
const obj = parseUrl(url);
if (!obj.protocol && !obj.hostname) {
return null;
try {
const obj = parseUrl(url);
if (!obj.protocol && !obj.hostname) {
return null;
}
return `${obj.protocol}//${obj.hostname}${obj.port ? `:${obj.port}` : ''}`;
} catch (error) {
return url; // Safe fallback to original url
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as commented in L149 - "Returns the origin (protocol + host + port) from given url if url is a valid absolute url, or null otherwise" it should return null when error is caught since it's likely a malformed url

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @ZilongX, should better to return null if the url failed to parse

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, we should better handle here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does sound better - I'll make the change.

}
return `${obj.protocol}//${obj.hostname}${obj.port ? `:${obj.port}` : ''}`;
}
8 changes: 8 additions & 0 deletions src/core/server/http/base_path_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ describe('BasePath', () => {

expect(basePath.prepend('http://localhost:5601/a/b')).toBe('http://localhost:5601/a/b');
});

it('returns path unchanged for prepend with malformed auth in url', () => {
const basePath = new BasePath('a/b');

expect(basePath.prepend('http://user:%E0%[email protected]')).toBe(
'http://user:%E0%[email protected]'
);
});
});

describe('#remove()', () => {
Expand Down
Loading