Skip to content

Conversation

@chdeskur
Copy link
Collaborator

@chdeskur chdeskur commented Nov 3, 2025

No description provided.

@vercel
Copy link
Contributor

vercel bot commented Nov 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dev.ferndocs.com Ready Ready Preview Nov 3, 2025 1:23pm
fern-dashboard Ready Ready Preview Nov 3, 2025 1:23pm
fern-dashboard-dev Ready Ready Preview Nov 3, 2025 1:23pm
ferndocs.com Building Building Preview Nov 3, 2025 1:23pm
preview.ferndocs.com Ready Ready Preview Nov 3, 2025 1:23pm
prod-assets.ferndocs.com Ready Ready Preview Nov 3, 2025 1:23pm
prod.ferndocs.com Ready Ready Preview Nov 3, 2025 1:23pm
1 Skipped Deployment
Project Deployment Preview Updated (UTC)
fern-platform Ignored Ignored Nov 3, 2025 1:23pm

{/* Fallback for users with JavaScript disabled */}
<noscript
dangerouslySetInnerHTML={{
__html: `<link rel="stylesheet" href="${href}" crossorigin="${crossOrigin}" />`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The href and crossOrigin props are directly interpolated into HTML without escaping, creating an XSS vulnerability through dangerouslySetInnerHTML.

View Details
📝 Patch Details
diff --git a/packages/fern-docs/bundle/src/components/AsyncStylesheet.tsx b/packages/fern-docs/bundle/src/components/AsyncStylesheet.tsx
index dd91da19d..785fd357e 100644
--- a/packages/fern-docs/bundle/src/components/AsyncStylesheet.tsx
+++ b/packages/fern-docs/bundle/src/components/AsyncStylesheet.tsx
@@ -8,32 +8,69 @@
  * maintaining React/TypeScript compatibility.
  */
 
+/**
+ * Validates and sanitizes a URL to prevent XSS attacks
+ * Returns null if the URL is invalid or potentially malicious
+ */
+function validateStylesheetUrl(href: string): string | null {
+    try {
+        // Only allow http/https URLs
+        const url = new URL(href, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
+        if (url.protocol !== 'http:' && url.protocol !== 'https:') {
+            return null;
+        }
+        return url.toString();
+    } catch {
+        return null;
+    }
+}
+
+/**
+ * Validates crossOrigin value to ensure it's one of the allowed values
+ */
+function validateCrossOrigin(crossOrigin: string): "anonymous" | "use-credentials" | null {
+    if (crossOrigin === "anonymous" || crossOrigin === "use-credentials") {
+        return crossOrigin;
+    }
+    return null;
+}
+
 interface AsyncStylesheetProps {
     href: string;
     crossOrigin?: "anonymous" | "use-credentials";
 }
 
 export function AsyncStylesheet({ href, crossOrigin = "anonymous" }: AsyncStylesheetProps) {
+    // Validate and sanitize inputs to prevent XSS
+    const validatedHref = validateStylesheetUrl(href);
+    const validatedCrossOrigin = validateCrossOrigin(crossOrigin);
+
+    // If validation fails, don't render anything to prevent XSS
+    if (!validatedHref || !validatedCrossOrigin) {
+        console.warn('AsyncStylesheet: Invalid href or crossOrigin prop detected, skipping render for security');
+        return null;
+    }
+
     return (
         <>
             {/* Preload hint for early resource discovery */}
-            <link rel="preload" as="style" href={href} crossOrigin={crossOrigin} />
+            <link rel="preload" as="style" href={validatedHref} crossOrigin={validatedCrossOrigin} />
 
             {/* Async loading with media swap trick - React's onLoad works here */}
             <link
                 rel="stylesheet"
-                href={href}
+                href={validatedHref}
                 media="print"
-                crossOrigin={crossOrigin}
+                crossOrigin={validatedCrossOrigin}
                 onLoad={(e) => {
                     (e.target as HTMLLinkElement).media = "all";
                 }}
             />
 
-            {/* Fallback for users with JavaScript disabled */}
+            {/* Fallback for users with JavaScript disabled - now safe because we validated inputs */}
             <noscript
                 dangerouslySetInnerHTML={{
-                    __html: `<link rel="stylesheet" href="${href}" crossorigin="${crossOrigin}" />`
+                    __html: `<link rel="stylesheet" href="${validatedHref}" crossorigin="${validatedCrossOrigin}" />`
                 }}
             />
         </>

Analysis

XSS vulnerability in AsyncStylesheet component via dangerouslySetInnerHTML

What fails: AsyncStylesheet component in packages/fern-docs/bundle/src/components/AsyncStylesheet.tsx directly interpolates untrusted href and crossOrigin props into HTML via dangerouslySetInnerHTML, allowing script execution

How to reproduce:

<AsyncStylesheet 
  href='x" onload="alert(1)'
  crossOrigin="anonymous" 
/>

Result: Generated HTML contains executable JavaScript: <link rel="stylesheet" href="x" onload="alert(1)" crossorigin="anonymous" /> which executes when parsed

Expected: Input should be validated/sanitized to prevent XSS attacks per React security guidelines

@chdeskur chdeskur merged commit c8aeae6 into app Nov 4, 2025
20 checks passed
@chdeskur chdeskur deleted the cd/onload-fix branch November 4, 2025 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants