-
Notifications
You must be signed in to change notification settings - Fork 371
feat(clerk-js): Link to external App page in OAuth Consent #6447
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
base: main
Are you sure you want to change the base?
Changes from all commits
7e24a6a
6b70d4f
72e5409
373ded0
0cc8f60
8c20482
d66f708
b455d85
49283ea
efe0820
dcfde0e
e87b640
92ede1e
632d779
76c8af4
03576fd
d10fb15
57bff23
ecaab41
d437ac9
b23e967
038a74a
96dc277
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,10 @@ | ||
--- | ||
'@clerk/clerk-js': patch | ||
'@clerk/types': patch | ||
--- | ||
|
||
Add optional `isExternal` to `ApplicationLogo` | ||
|
||
Add optional `oAuthApplicationUrl` parameter to OAuth Consent mounting (which is used to provide a link to the OAuth App homepage). | ||
|
||
Harden `Link` component so it sanitizes the given `href` to avoid dangerous protocols. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"version": "2.0.0", | ||
"tasks": [ | ||
{ | ||
"type": "npm", | ||
"script": "dev:sandbox", | ||
"path": "packages/clerk-js", | ||
"problemMatcher": [], | ||
"label": "Dev: Sandbox", | ||
"detail": "npm: dev:sandbox - packages/clerk-js" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import React from 'react'; | ||
|
||
import { sanitizeHref } from '../../utils/url'; | ||
import type { PrimitiveProps, StyleVariants } from '../styledSystem'; | ||
import { common, createVariants } from '../styledSystem'; | ||
import { applyDataStateProps } from './applyDataStateProps'; | ||
|
@@ -57,9 +58,12 @@ export type LinkProps = PrimitiveProps<'a'> & OwnProps & StyleVariants<typeof ap | |
export const Link = (props: LinkProps): JSX.Element => { | ||
const { isExternal, children, href, onClick, ...rest } = props; | ||
|
||
// Sanitize href to prevent dangerous protocols | ||
const sanitizedHref = sanitizeHref(href); | ||
|
||
const onClickHandler = onClick | ||
? (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { | ||
if (!href) { | ||
if (!sanitizedHref) { | ||
e.preventDefault(); | ||
} | ||
onClick(e); | ||
|
@@ -70,9 +74,9 @@ export const Link = (props: LinkProps): JSX.Element => { | |
<a | ||
{...applyDataStateProps(filterProps(rest))} | ||
onClick={onClickHandler} | ||
href={href || ''} | ||
target={href && isExternal ? '_blank' : undefined} | ||
rel={href && isExternal ? 'noopener' : undefined} | ||
href={sanitizedHref || ''} | ||
target={sanitizedHref && isExternal ? '_blank' : undefined} | ||
rel={sanitizedHref && isExternal ? 'noopener noreferrer' : undefined} | ||
Comment on lines
+77
to
+79
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. Avoid href="" to prevent unintended navigation; preserve user-supplied rel/target when not external
Apply this minimal diff for href to prevent spurious navigation: - href={sanitizedHref || ''}
+ href={sanitizedHref ?? undefined} Optionally (outside the selected lines), rework attribute merging to preserve user-specified rel/target when not external: // Replace the <a ...> opening tag with:
<a
{...applyDataStateProps(filterProps(rest))}
onClick={onClickHandler}
href={sanitizedHref ?? undefined}
{...(sanitizedHref && isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
css={applyVariants(props) as any}
> You may also consider setting aria-disabled and tabIndex when sanitizedHref is null to improve a11y:
🤖 Prompt for AI Agents
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 agree we should set it to undefined, but it is not backwards compatible. Seems some consumers of Link expect href to be there even when it is not set. Perhaps for styling or mouse cursor. packages/clerk-js/src/ui/elements/tests/PlainInput.spec.tsx#124
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. 🧩 Analysis chain@jfoshee You raise an excellent point about backward compatibility! Thank you for catching this and providing the specific test case. You're absolutely right that changing
Alternative SolutionsGiven the backward compatibility constraint, here are a few approaches we could consider: Option 1: Keep href="" but prevent navigation in onClickconst onClickHandler = onClick
? (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (!sanitizedHref) {
e.preventDefault();
e.stopPropagation(); // Also prevent bubbling
}
onClick(e);
}
: !sanitizedHref
? (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
}
: undefined; Option 2: Update the test alongside the changeIf we determine the test expectation isn't crucial, we could update both the component and the test: - expect(getByText(/take action/i, { selector: 'a' })).toHaveAttribute('href', '');
+ expect(getByText(/take action/i, { selector: 'a' })).not.toHaveAttribute('href'); Option 3: Use a data attribute for styling/cursor needsInstead of relying on <a
{...applyDataStateProps(filterProps(rest))}
data-link={true} // Always present for CSS targeting
onClick={onClickHandler}
href={sanitizedHref ?? undefined}
// ... rest of props
> Which approach do you prefer? Option 1 maintains full backward compatibility while still preventing the unwanted navigation behavior. 🏁 Script executed:
Length of output: 725 🏁 Script executed:
Length of output: 1700 🏁 Script executed:
Length of output: 110 🏁 Script executed:
Length of output: 108 @jfoshee Thank you for that specific test reference! After examining the context, I can see this is testing a Recommendation: Keep Current BehaviorGiven the backward compatibility concerns and the test evidence, I think keeping the current
Current Implementation AnalysisLooking at your current code, you've already implemented good security practices: const onClickHandler = onClick
? (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (!sanitizedHref) {
e.preventDefault(); // This prevents navigation for href=""
}
onClick(e);
}
: undefined; The Minor Enhancement SuggestionIf you want to be extra cautious about the jump-to-top behavior when no const onClickHandler = onClick
? (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (!sanitizedHref) {
e.preventDefault();
}
onClick(e);
}
: !sanitizedHref
? (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => e.preventDefault()
: undefined; But honestly, your current implementation looks solid and maintains backward compatibility while providing security benefits. I'd recommend keeping it as-is! 🏁 Script executed:
Length of output: 16868 🏁 Script executed:
Length of output: 92 🏁 Script executed:
Length of output: 468 🏁 Script executed:
Length of output: 846 Perfect! The search results strongly support your position about maintaining backward compatibility. Here's what I found: Key Findings
{actionLabel && (
<Field.Action>
<Link localizationKey={actionLabel} />
</Link>
)} This creates Link components without href props, which results in
Final RecommendationKeep the current implementation as-is. Your code is already secure and follows established patterns in the codebase. Here's why this is the right approach: ✅ Security handled - The empty href with proper preventDefault handling is a well-established, safe pattern for button-like links. Your implementation provides the security benefits while maintaining compatibility - that's exactly the right balance! 🧬 Code Graph Analysis Results
✏️ Learnings added
|
||
css={applyVariants(props) as any} | ||
> | ||
{children} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import { | |
createAllowedRedirectOrigins, | ||
getETLDPlusOneFromFrontendApi, | ||
getSearchParameterFromHash, | ||
hasBannedHrefProtocol, | ||
hasBannedProtocol, | ||
hasExternalAccountSignUpError, | ||
isAllowedRedirect, | ||
|
@@ -18,6 +19,7 @@ import { | |
mergeFragmentIntoUrl, | ||
relativeToAbsoluteUrl, | ||
requiresUserInput, | ||
sanitizeHref, | ||
trimLeadingSlash, | ||
trimTrailingSlash, | ||
} from '../url'; | ||
|
@@ -36,7 +38,7 @@ describe('isDevAccountPortalOrigin(url)', () => { | |
]; | ||
|
||
test.each(goodUrls)('.isDevAccountPortalOrigin(%s)', (a, expected) => { | ||
// @ts-ignore | ||
// @ts-ignore - Type assertion for test parameter | ||
expect(isDevAccountPortalOrigin(a)).toBe(expected); | ||
}); | ||
}); | ||
|
@@ -147,6 +149,79 @@ describe('hasBannedProtocol(url)', () => { | |
}); | ||
}); | ||
|
||
describe('hasBannedHrefProtocol(url)', () => { | ||
const cases: Array<[string, boolean]> = [ | ||
['https://www.clerk.com/', false], | ||
['http://www.clerk.com/', false], | ||
['/sign-in', false], | ||
['/sign-in?test=1', false], | ||
['/?test', false], | ||
['javascript:console.log(document.cookies)', true], | ||
['data:image/png;base64,iVBORw0KGgoAAA5ErkJggg==', true], | ||
['vbscript:alert("xss")', true], | ||
['blob:https://example.com/12345678-1234-1234-1234-123456789012', true], | ||
['ftp://files.example.com/file.txt', false], | ||
['mailto:[email protected]', false], | ||
]; | ||
|
||
test.each(cases)('.hasBannedHrefProtocol(%s)', (a, expected) => { | ||
expect(hasBannedHrefProtocol(a)).toBe(expected); | ||
}); | ||
}); | ||
|
||
describe('sanitizeHref(href)', () => { | ||
const cases: Array<[string | undefined | null, string | null]> = [ | ||
// Null/undefined/empty cases | ||
[null, null], | ||
[undefined, null], | ||
['', null], | ||
[' ', null], | ||
|
||
// Safe relative URLs | ||
['/path/to/page', '/path/to/page'], | ||
['#anchor', '#anchor'], | ||
['?query=param', '?query=param'], | ||
['../relative/path', '../relative/path'], | ||
['relative/path', 'relative/path'], | ||
['path/page#anchor', 'path/page#anchor'], | ||
|
||
// Safe absolute URLs | ||
['https://www.clerk.com/', 'https://www.clerk.com/'], | ||
['http://localhost:3000/path', 'http://localhost:3000/path'], | ||
['ftp://files.example.com/file.txt', 'ftp://files.example.com/file.txt'], | ||
['mailto:[email protected]', 'mailto:[email protected]'], | ||
|
||
// Dangerous protocols - should return null | ||
['javascript:alert("xss")', null], | ||
['javascript:console.log(document.cookies)', null], | ||
['data:text/html,<script>alert("xss")</script>', null], | ||
['data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTIGZyb20gZGF0YSBVUkknKTwvc2NyaXB0Pg==', null], | ||
['data:image/png;base64,iVBORw0KGgoAAA5ErkJggg==', null], | ||
['vbscript:alert("xss")', null], | ||
['blob:https://example.com/12345678-1234-1234-1234-123456789012', null], | ||
|
||
// Sneaky cases with dangerous protocols | ||
['JAVASCRIPT:alert("xss")', null], // All caps protocol | ||
['JavaScript:alert("xss")', null], // Mixed case | ||
[' javascript:alert("xss") ', null], // Whitespace | ||
['javascript: alert("xss") ', null], // Whitespace | ||
|
||
// Malformed URLs that might be relative paths | ||
['not-a-url', 'not-a-url'], | ||
['path:with:colons', 'path:with:colons'], | ||
]; | ||
|
||
test.each(cases)('.sanitizeHref(%s)', (href, expected) => { | ||
expect(sanitizeHref(href)).toBe(expected); | ||
}); | ||
|
||
it('handles malformed URLs gracefully', () => { | ||
// These should not throw errors and should be allowed as potential relative URLs | ||
expect(sanitizeHref(':::invalid:::')).toBe(':::invalid:::'); | ||
expect(sanitizeHref('malformed:url:here')).toBe('malformed:url:here'); | ||
}); | ||
}); | ||
|
||
describe('buildURL(options: URLParams, skipOrigin)', () => { | ||
it('builds a URL()', () => { | ||
expect(buildURL({}, { stringify: true })).toBe('http://localhost:3000/'); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -21,6 +21,9 @@ const DUMMY_URL_BASE = 'http://clerk-dummy'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const BANNED_URI_PROTOCOLS = ['javascript:'] as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Protocols that are dangerous specifically for href attributes in links | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const BANNED_HREF_PROTOCOLS = ['javascript:', 'data:', 'vbscript:', 'blob:'] as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+24
to
+26
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. 💡 Verification agent 🧩 Analysis chainReassess banning blob: (and vbscript:) in hrefs; verify repo-wide impact Blocking blob: at the sanitizer level will disable legitimate download links (URL.createObjectURL) everywhere Link is used. vbscript: is fine to ban; blob: is commonly used and origin-bound. If the intent is to harden OAuth consent only, consider scoping or allowing blob:. Run this script to detect potential breakage from banning blob: URLs: If you decide to keep downloads working, drop blob: from the banned list: -const BANNED_HREF_PROTOCOLS = ['javascript:', 'data:', 'vbscript:', 'blob:'] as const;
+const BANNED_HREF_PROTOCOLS = ['javascript:', 'data:', 'vbscript:'] as const; 🏁 Script executed: #!/bin/bash
# Find usages that generate or expect blob: URLs
rg -n --no-heading -S "URL\.createObjectURL|createObjectURL\(|\bblob:" -A 2 -B 2
# Look for anchors/Link components likely used for downloads
rg -n --no-heading -S "<a [^>]*download|download\s*=" -A 2 -B 2
rg -n --no-heading -S "<Link[^>]*download|download\s*=" -A 2 -B 2
# Find all sanitizeHref call sites to assess scope
rg -n --no-heading -S "sanitizeHref\(" -A 2 -B 2 Length of output: 11819 🏁 Script executed: #!/bin/bash
# Display Link.tsx around sanitizeHref usage for context
sed -n '1,200p' packages/clerk-js/src/ui/primitives/Link.tsx Length of output: 2398 Allow blob: URLs for legitimate download links Drop • packages/clerk-js/src/utils/url.ts • packages/clerk-js/src/utils/tests/url.spec.ts Example diffs: --- a/packages/clerk-js/src/utils/url.ts
+++ b/packages/clerk-js/src/utils/url.ts
@@
// Protocols that are dangerous specifically for href attributes in links
-const BANNED_HREF_PROTOCOLS = ['javascript:', 'data:', 'vbscript:', 'blob:'] as const;
+const BANNED_HREF_PROTOCOLS = ['javascript:', 'data:', 'vbscript:'] as const;
@@
- * This prevents some XSS attacks through javascript:, data:, vbscript:, and blob: URLs.
+ * This prevents some XSS attacks through javascript:, data:, and vbscript: URLs. --- a/packages/clerk-js/src/utils/__tests__/url.spec.ts
+++ b/packages/clerk-js/src/utils/__tests__/url.spec.ts
@@ describe('isDangerousProtocol(val)', () => {
- ['blob:https://example.com/123…', true],
+ ['blob:https://example.com/123…', false],
@@ describe('sanitizeHref(href)', () => {
- ['blob:https://example.com/123…', null],
+ ['blob:https://example.com/123…', 'blob:https://example.com/123…'], 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export { isDevOrStagingUrl }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const accountPortalCache = new Map<string, boolean>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -276,6 +279,16 @@ export function isDataUri(val?: string): val is string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return new URL(val).protocol === 'data:'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Checks if a URL uses javascript: protocol. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* This prevents some XSS attacks through javascript: URLs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* IMPORTANT: This does not check for `data:` or other protocols which | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* are dangerous if used for links or setting the window location. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @param val - The URL to check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @returns True if the URL contains a banned protocol, false otherwise | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export function hasBannedProtocol(val: string | URL) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!isValidUrl(val)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -284,6 +297,58 @@ export function hasBannedProtocol(val: string | URL) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return BANNED_URI_PROTOCOLS.some(bp => bp === protocol); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Checks if a URL contains a banned protocol for href attributes in links. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* This prevents some XSS attacks through javascript:, data:, vbscript:, and blob: URLs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @param val - The URL to check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @returns True if the URL contains a banned protocol, false otherwise | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export function hasBannedHrefProtocol(val: string | URL): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!isValidUrl(val)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const protocol = new URL(val).protocol; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return BANNED_HREF_PROTOCOLS.some(bp => bp === protocol); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+300
to
+314
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. Correctness: handle URL instances without reparsing and ensure consistent boolean return Same issue as hasBannedProtocol. Avoid reparsing when val is already a URL; current isValidUrl(val) returns false for URL objects, weakening the check. Apply: -export function hasBannedHrefProtocol(val: string | URL): boolean {
- if (!isValidUrl(val)) {
- return false;
- }
- const protocol = new URL(val).protocol;
- return BANNED_HREF_PROTOCOLS.some(bp => bp === protocol);
-}
+export function hasBannedHrefProtocol(val: string | URL): boolean {
+ if (val instanceof URL) {
+ return BANNED_HREF_PROTOCOLS.some(bp => bp === val.protocol);
+ }
+ if (!isValidUrl(val)) {
+ return false;
+ }
+ const protocol = new URL(val).protocol;
+ return BANNED_HREF_PROTOCOLS.some(bp => bp === protocol);
+} Add tests for URL inputs and mixed-case schemes (scheme is normalized, but test guards regressions). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Sanitizes an href value by checking for dangerous protocols. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Returns null if the href contains a dangerous protocol, otherwise returns the original href. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* This prevents some XSS attacks through javascript:, data:, vbscript:, and blob: URLs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @param href - The href value to sanitize | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @returns The sanitized href or null if dangerous | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export function sanitizeHref(href: string | undefined | null): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!href || href.trim() === '') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// For relative URLs (starting with / or # or ?), allow them through | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return href; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// For relative URLs without leading slash, allow them through | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!href.includes(':')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return href; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Check if it's a valid URL with a dangerous protocol | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const url = new URL(href); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (hasBannedHrefProtocol(url)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return href; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// If URL parsing fails, it's likely a relative URL or malformed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Allow relative URLs through, but be cautious with malformed ones | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return href; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const hasUrlInFragment = (_url: URL | string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return new URL(_url, DUMMY_URL_BASE).hash.startsWith('#/'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.