-
Notifications
You must be signed in to change notification settings - Fork 446
chore(backend): Harden joinPaths against traversal
#8331
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
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@clerk/backend': patch | ||
| --- | ||
|
|
||
| Add path traversal protections in `joinPaths` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,43 @@ | ||
| const SEPARATOR = '/'; | ||
| const MULTIPLE_SEPARATOR_REGEX = new RegExp('(?<!:)' + SEPARATOR + '{1,}', 'g'); | ||
| const MAX_DECODES = 10; | ||
|
|
||
| type PathString = string | null | undefined; | ||
|
|
||
| function isDotSegment(segment: string): boolean { | ||
| let candidate = segment; | ||
| for (let i = 0; i <= MAX_DECODES; i++) { | ||
| // After decoding, check if any slash-separated part is a dot segment | ||
| if (candidate.split(/[/\\]/).some(p => p === '.' || p === '..')) { | ||
| return true; | ||
| } | ||
| if (i === MAX_DECODES) { | ||
| throw new Error(`joinPaths: too many layers of encoding in ${segment}`); | ||
| } | ||
| try { | ||
| const next = decodeURIComponent(candidate); | ||
| if (next === candidate) { | ||
| break; | ||
| } // stable — no more encoding | ||
| candidate = next; | ||
| } catch { | ||
| break; | ||
| } | ||
| } | ||
| return false; | ||
|
Member
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. I think CodeRabbit is right about this one. The loop runs
Contributor
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. Oooh right I misread the finding and thought he meant we'd miss out on the
Contributor
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. I fixed it and changed the approach a bit. We decode until it's clean and there's a max number of 10 iterations where it becomes just silly and for sure someone's doing something strange and we reject it. |
||
| } | ||
|
dominic-clerk marked this conversation as resolved.
|
||
|
|
||
| export function joinPaths(...args: PathString[]): string { | ||
| return args | ||
| const result = args | ||
| .filter(p => p) | ||
| .join(SEPARATOR) | ||
| .replace(MULTIPLE_SEPARATOR_REGEX, SEPARATOR); | ||
|
|
||
| for (const segment of result.split(SEPARATOR)) { | ||
| if (isDotSegment(segment)) { | ||
| throw new Error(`joinPaths: "." and ".." path segments are not allowed (received "${result}")`); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
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.
Minor maybe? I wasn't sure