-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Use spec-compliant URL parser to parse next
URL parameter.
#183521
Changes from 2 commits
7410e34
8fea2be
3e55db9
96772c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,23 @@ describe('isInternalURL', () => { | |
const href = `${basePath}/app/kibana/../../../management`; | ||
expect(isInternalURL(href, basePath)).toBe(false); | ||
}); | ||
|
||
it('should return `false` if absolute URL contains tabs or new lines', () => { | ||
for (const char of ['\t', '\n', '\r', '\t\n\r']) { | ||
for (const url of [ | ||
`/${char}/${basePath}app/kibana`, | ||
`/${char}//${basePath}app/kibana`, | ||
`//${char}/${basePath}app/kibana`, | ||
`htt${char}ps://example.com${basePath}`, | ||
`htt${char}p://example.org:5601${basePath}`, | ||
`file${char}://example.com${basePath}`, | ||
`htt${char}p://example.org:5601${char}${basePath}`, | ||
`https:/${char}/example.com${char}${basePath}`, | ||
]) { | ||
expect(isInternalURL(url, basePath)).toBe(false); | ||
} | ||
} | ||
}); | ||
}); | ||
|
||
describe('without basePath defined', () => { | ||
|
@@ -86,5 +103,34 @@ describe('isInternalURL', () => { | |
const hrefWithThreeSlashes = `///app/kibana`; | ||
expect(isInternalURL(hrefWithThreeSlashes)).toBe(false); | ||
}); | ||
|
||
it('should return `false` if absolute URL contains tabs or new lines', () => { | ||
for (const char of ['\t', '\n', '\r', '\t\n\r']) { | ||
for (const url of [ | ||
`/${char}/app/kibana`, | ||
`/${char}//app/kibana`, | ||
`//${char}/app/kibana`, | ||
`/${char}/example.com`, | ||
`/${char}//example.com`, | ||
`//${char}/example.com`, | ||
`htt${char}ps://example.com`, | ||
`/${char}/example.org`, | ||
`/${char}//example.org`, | ||
`//${char}/example.org`, | ||
`htt${char}ps://example.org`, | ||
`/${char}/example.org:5601`, | ||
`/${char}//example.org:5601`, | ||
`//${char}/example.org:5601`, | ||
`htt${char}ps://example.org:5601`, | ||
`java${char}script:alert(1)`, | ||
`htt${char}p://example.org:5601`, | ||
`https:/${char}/example.com`, | ||
`file${char}://example.com`, | ||
`https://example${char}.com/path`, | ||
]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. These would all return false, even without the special character. Should we have at least one test case that would otherwise return true, if not for the tab/newline? I tried this with using '/app/kibana#/discover/New-Saved-Search' (from the true test case) and adding the special char in various places, but it still returned true. Am I misinterpreting the test? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Without the special character, they will behave as expected with both the current and previous code and return Does that make sense?
Yeah, I think it's a good idea. What about having a similar loop with a subset of the URLs tested here, but with a white space instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I've made an attempt to make the intention a bit clearer in unit tests in 96772c3. @jeramysoucy let me know if it helps. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is much more clear to me now. Thanks, Oleg! |
||
expect(isInternalURL(url)).toBe(false); | ||
} | ||
} | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,37 +6,35 @@ | |
* Side Public License, v 1. | ||
*/ | ||
|
||
import { parse as parseUrl } from 'url'; | ||
|
||
/** | ||
* Determine if url is outside of this Kibana install. | ||
*/ | ||
export function isInternalURL(url: string, basePath = '') { | ||
const { protocol, hostname, port, pathname } = parseUrl( | ||
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) { | ||
// We use the WHATWG parser TWICE with completely different dummy base URLs to ensure that the parsed URL always | ||
// inherits the origin of the base URL. This means that the specified URL isn't an absolute URL, or a scheme-relative | ||
// URL (//), or a scheme-relative URL with an empty host (///). Browsers may process such URLs unexpectedly due to | ||
// backward compatibility reasons (e.g., a browser may treat `///abc.com` as just `abc.com`). For more details, refer | ||
// to https://url.spec.whatwg.org/#concept-basic-url-parser and https://url.spec.whatwg.org/#url-representation. | ||
let normalizedURL: URL; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: hopefully, we can get rid of this trickery when/if this is accepted and implemented: whatwg/url#531 |
||
try { | ||
for (const baseURL of ['http://example.org:5601', 'https://example.com']) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. QQ: why do we need the one with the port there? isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on what we know today, it's redundant, agreed. The idea was to have two base URLs that differ in all three parts of the origin (protocol, host, and port) just to ensure there is no way to exploit any backward compatibility browser quirk that we're not aware of yet to make the browser treat a relative URL as an absolute one. |
||
normalizedURL = new URL(url, baseURL); | ||
if (normalizedURL.origin !== baseURL) { | ||
return false; | ||
} | ||
} | ||
} catch { | ||
return false; | ||
} | ||
|
||
// Now we need to normalize URL to make sure any relative path segments (`..`) cannot escape expected base path. | ||
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}/`)) | ||
// the original URL too. We can safely use non-null assertion operator here since we know `normalizedURL` is | ||
// always defined, otherwise we would have returned `false` already. | ||
url.startsWith('/') && | ||
(normalizedURL!.pathname === basePath || normalizedURL!.pathname.startsWith(`${basePath}/`)) | ||
); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't these all return false anyway, even without the special character? Should we have at least one test case that would otherwise return true, if not for the tab/newline?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've explained the idea in https://github.com/elastic/kibana/pull/183521/files#r1603774927, but in this specific case (with the base path) you're right - these all seem to return
false
even with the previous code, which is not what we want. Let me take a look.