-
Notifications
You must be signed in to change notification settings - Fork 22
chore: load katex with async stylesheet #4680
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 |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { Providers } from "@fern-docs/components/providers/providers"; | |
| import type { Metadata, Viewport } from "next/types"; | ||
| import { experimental_taintUniqueValue } from "react"; | ||
|
|
||
| import { AsyncStylesheet } from "@/components/AsyncStylesheet"; | ||
| import { ConsoleMessage } from "@/components/console-message"; | ||
| import { WebSocketRefresh } from "@/components/websocket-refresh"; | ||
|
|
||
|
|
@@ -56,23 +57,10 @@ export default function Layout({ children }: { children: React.ReactNode }) { | |
| <link rel="dns-prefetch" href="https://cdn.jsdelivr.net" /> | ||
|
|
||
| {/* Non-blocking KaTeX CSS loading */} | ||
| <link | ||
| rel="preload" | ||
| as="style" | ||
| <AsyncStylesheet | ||
| href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" | ||
| crossOrigin="anonymous" | ||
| /> | ||
| <link | ||
| rel="stylesheet" | ||
| href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" | ||
| media="print" | ||
| onload="this.media='all'" | ||
| /> | ||
| <noscript | ||
| dangerouslySetInnerHTML={{ | ||
| __html: '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" />' | ||
| }} | ||
| /> | ||
| </head> | ||
| ); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| "use client"; | ||
|
|
||
| /** | ||
| * AsyncStylesheet - Non-blocking CSS loading component | ||
| * | ||
| * Loads a stylesheet asynchronously without blocking page render. | ||
| * Uses the media="print" + onLoad trick for optimal performance while | ||
| * maintaining React/TypeScript compatibility. | ||
| */ | ||
|
|
||
| interface AsyncStylesheetProps { | ||
| href: string; | ||
| crossOrigin?: "anonymous" | "use-credentials"; | ||
| } | ||
|
|
||
| export function AsyncStylesheet({ href, crossOrigin = "anonymous" }: AsyncStylesheetProps) { | ||
| return ( | ||
| <> | ||
| {/* Preload hint for early resource discovery */} | ||
| <link rel="preload" as="style" href={href} crossOrigin={crossOrigin} /> | ||
|
|
||
| {/* Async loading with media swap trick - React's onLoad works here */} | ||
| <link | ||
| rel="stylesheet" | ||
| href={href} | ||
| media="print" | ||
| crossOrigin={crossOrigin} | ||
| onLoad={(e) => { | ||
| (e.target as HTMLLinkElement).media = "all"; | ||
| }} | ||
| /> | ||
|
|
||
| {/* Fallback for users with JavaScript disabled */} | ||
| <noscript | ||
| dangerouslySetInnerHTML={{ | ||
| __html: `<link rel="stylesheet" href="${href}" crossorigin="${crossOrigin}" />` | ||
|
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. The View Details📝 Patch Detailsdiff --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}" />`
}}
/>
</>
AnalysisXSS vulnerability in AsyncStylesheet component via dangerouslySetInnerHTMLWhat fails: AsyncStylesheet component in How to reproduce: <AsyncStylesheet
href='x" onload="alert(1)'
crossOrigin="anonymous"
/>Result: Generated HTML contains executable JavaScript: Expected: Input should be validated/sanitized to prevent XSS attacks per React security guidelines |
||
| }} | ||
| /> | ||
| </> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.