(path: string)
| 633 | } |
| 634 | |
| 635 | export function decodePath(path: string) { |
| 636 | if (!path) return { path, handledProtocolRelativeURL: false } |
| 637 | |
| 638 | // Fast path: most paths are already decoded and safe. |
| 639 | // Only fall back to the slower scan/regex path when we see a '%' (encoded), |
| 640 | // a backslash (explicitly handled), a control character, or a protocol-relative |
| 641 | // prefix which needs collapsing. |
| 642 | // eslint-disable-next-line no-control-regex |
| 643 | if (!/[%\\\x00-\x1f\x7f]/.test(path) && !path.startsWith('//')) { |
| 644 | return { path, handledProtocolRelativeURL: false } |
| 645 | } |
| 646 | |
| 647 | const re = /%25|%5C/gi |
| 648 | let cursor = 0 |
| 649 | let result = '' |
| 650 | let match |
| 651 | while (null !== (match = re.exec(path))) { |
| 652 | result += decodeSegment(path.slice(cursor, match.index)) + match[0] |
| 653 | cursor = re.lastIndex |
| 654 | } |
| 655 | result = result + decodeSegment(cursor ? path.slice(cursor) : path) |
| 656 | |
| 657 | // Prevent open redirect via protocol-relative URLs (e.g. "//evil.com") |
| 658 | // This is defense-in-depth: since control characters are no longer decoded, |
| 659 | // paths like "/%0d/evil.com" can no longer become "//evil.com". But we keep |
| 660 | // this check to guard against other edge cases. |
| 661 | let handledProtocolRelativeURL = false |
| 662 | if (result.startsWith('//')) { |
| 663 | handledProtocolRelativeURL = true |
| 664 | result = '/' + result.replace(/^\/+/, '') |
| 665 | } |
| 666 | |
| 667 | return { path: result, handledProtocolRelativeURL } |
| 668 | } |
| 669 | |
| 670 | /** |
| 671 | * Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing. |
no test coverage detected