-
Notifications
You must be signed in to change notification settings - Fork 453
fix(clerk-js): Directory traversal relative URL detection #4483
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f463f50
fix an issue with relative URL detection
brkalow 1cfec87
chore(clerk-js): Add more edge cases
nikosdouvlis 56ffffd
chore(clerk-js): Add more edge cases
nikosdouvlis 64dcf23
refactor allowed redirect logic
brkalow 71fa0e0
ensure parse returns strings
brkalow b678d48
fix numbering
brkalow cb9f3a1
add additional test case
brkalow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@clerk/clerk-js': patch | ||
| --- | ||
|
|
||
| Fix an issue where protocol relative URLs were not properly detected as non-relative. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -234,37 +234,38 @@ export function isValidUrl(val: unknown): val is string { | |
| } | ||
| } | ||
|
|
||
| export function relativeToAbsoluteUrl(url: string, origin: string | URL): string { | ||
| if (isValidUrl(url)) { | ||
| return url; | ||
| export function relativeToAbsoluteUrl(url: string, origin: string | URL): URL { | ||
| try { | ||
| return new URL(url); | ||
| } catch (e) { | ||
| return new URL(url, origin); | ||
| } | ||
| return new URL(url, origin).href; | ||
| } | ||
|
|
||
| export function isRelativeUrl(val: string): boolean { | ||
| if (val !== val && !val) { | ||
| return false; | ||
| } | ||
| // Regular expression to detect disallowed patterns | ||
| const disallowedPatterns = [ | ||
| /\0/, // Null bytes | ||
| /^\/\//, // Protocol-relative | ||
| // eslint-disable-next-line no-control-regex | ||
| /[\x00-\x1F]/, // Control characters | ||
| ]; | ||
|
|
||
| if (val.startsWith('//') || val.startsWith('http/') || val.startsWith('https/')) { | ||
| // Protocol-relative URL; consider it absolute. | ||
| return false; | ||
| /** | ||
| * Check for potentially problematic URLs that could have been crafted to intentionally bypass the origin check. Note that the URLs passed to this | ||
| * function are assumed to be from an "allowed origin", so we are not executing origin-specific checks here. | ||
| */ | ||
| export function isProblematicUrl(url: URL): boolean { | ||
| if (hasBannedProtocol(url)) { | ||
| return true; | ||
| } | ||
|
|
||
| try { | ||
| // If this does not throw, it's a valid absolute URL | ||
| new URL(val); | ||
| return false; | ||
| } catch (e) { | ||
| try { | ||
| // If this does not throw, it's a valid relative URL | ||
| new URL(val, DUMMY_URL_BASE); | ||
| // Check against disallowed patterns | ||
| for (const pattern of disallowedPatterns) { | ||
| if (pattern.test(url.pathname)) { | ||
| return true; | ||
| } catch (e) { | ||
| // Invalid URL case | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| export function isDataUri(val?: string): val is string { | ||
|
|
@@ -352,20 +353,25 @@ export function requiresUserInput(redirectUrl: string): boolean { | |
| return frontendApiRedirectPathsWithUserInput.includes(url.pathname); | ||
| } | ||
|
|
||
| export const isAllowedRedirectOrigin = | ||
| (allowedRedirectOrigins: Array<string | RegExp> | undefined) => (_url: string) => { | ||
| if (!allowedRedirectOrigins) { | ||
| return true; | ||
| export const isAllowedRedirect = | ||
|
Member
Author
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. This function now deals with full URLs, instead of potentially relative URLs as well. This makes our checks more consistent, and removes situations where we treat a relative URL as okay, make it absolute, and then it ends up being problematic. |
||
| (allowedRedirectOrigins: Array<string | RegExp> | undefined, currentOrigin: string) => (_url: URL | string) => { | ||
| let url = _url; | ||
| if (typeof url === 'string') { | ||
| url = relativeToAbsoluteUrl(url, currentOrigin); | ||
| } | ||
|
|
||
| if (isRelativeUrl(_url)) { | ||
| if (!allowedRedirectOrigins) { | ||
| return true; | ||
| } | ||
|
|
||
| const url = new URL(_url, DUMMY_URL_BASE); | ||
| const isAllowed = allowedRedirectOrigins | ||
| .map(origin => (typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin)) | ||
| .some(origin => origin.test(trimTrailingSlash(url.origin))); | ||
| const isSameOrigin = currentOrigin === url.origin; | ||
|
|
||
| const isAllowed = | ||
| !isProblematicUrl(url) && | ||
| (isSameOrigin || | ||
| allowedRedirectOrigins | ||
| .map(origin => (typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin)) | ||
| .some(origin => origin.test(trimTrailingSlash(url.origin)))); | ||
|
|
||
| if (!isAllowed) { | ||
| logger.warnOnce( | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Could potentially do with more test cases here, but with the shift to checking against full URLs we get more guarantees from the existing origin check.