@@ -518,6 +518,64 @@ function decodeSegment(segment: string): string {
518518 return sanitizePathSegment ( decoded )
519519}
520520
521+ /**
522+ * List of URL protocols that are safe for navigation.
523+ * Only these protocols are allowed in redirects and navigation.
524+ */
525+ export const SAFE_URL_PROTOCOLS = [ 'http:' , 'https:' , 'mailto:' , 'tel:' ]
526+
527+ /**
528+ * Check if a URL string uses a protocol that is not in the safe list.
529+ * Returns true for dangerous protocols like javascript:, data:, vbscript:, etc.
530+ *
531+ * The URL constructor correctly normalizes:
532+ * - Mixed case (JavaScript: → javascript:)
533+ * - Whitespace/control characters (java\nscript: → javascript:)
534+ * - Leading whitespace
535+ *
536+ * For relative URLs (no protocol), returns false (safe).
537+ *
538+ * @param url - The URL string to check
539+ * @returns true if the URL uses a dangerous (non-whitelisted) protocol
540+ */
541+ export function isDangerousProtocol ( url : string ) : boolean {
542+ if ( ! url ) return false
543+
544+ try {
545+ // Use the URL constructor - it correctly normalizes protocols
546+ // per WHATWG URL spec, handling all bypass attempts automatically
547+ const parsed = new URL ( url )
548+ return ! SAFE_URL_PROTOCOLS . includes ( parsed . protocol )
549+ } catch {
550+ // URL constructor throws for relative URLs (no protocol)
551+ // These are safe - they can't execute scripts
552+ return false
553+ }
554+ }
555+
556+ // This utility is based on https://github.com/zertosh/htmlescape
557+ // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
558+ const HTML_ESCAPE_LOOKUP : { [ match : string ] : string } = {
559+ '&' : '\\u0026' ,
560+ '>' : '\\u003e' ,
561+ '<' : '\\u003c' ,
562+ '\u2028' : '\\u2028' ,
563+ '\u2029' : '\\u2029' ,
564+ }
565+
566+ const HTML_ESCAPE_REGEX = / [ & > < \u2028 \u2029 ] / g
567+
568+ /**
569+ * Escape HTML special characters in a string to prevent XSS attacks
570+ * when embedding strings in script tags during SSR.
571+ *
572+ * This is essential for preventing XSS vulnerabilities when user-controlled
573+ * content is embedded in inline scripts.
574+ */
575+ export function escapeHtml ( str : string ) : string {
576+ return str . replace ( HTML_ESCAPE_REGEX , ( match ) => HTML_ESCAPE_LOOKUP [ match ] ! )
577+ }
578+
521579export function decodePath ( path : string , decodeIgnore ?: Array < string > ) : string {
522580 if ( ! path ) return path
523581 const re = decodeIgnore
0 commit comments