feat: apply special classes on regex match in pages#20
Conversation
calebephrem
commented
Jul 8, 2026
- Apply special class on regex match in pages, such as markdown links, codeblocks and channel names
- Add link preview card component to show overlay on link hover
- Organize component files
|
@calebephrem is attempting to deploy a commit to the aditya ojha's projects Team on Vercel. A member of the Team first needs to authorize it. |
Summary by BeetleThis PR refactors the project structure and enhances the pages system with improved text rendering capabilities. The main focus is on moving configuration files to a more appropriate location ( 📁 File Changes Summary (Consolidated across all commits):
Total Changes: 13 files changed, +461 additions, -74 deletions 🗺️ Walkthrough:sequenceDiagram
participant User
participant PageClient
participant ApplySpecialClass
participant LinkPreviewCard
participant API as /api/link-preview
participant External as External Site
User->>PageClient: Hover over link in page content
PageClient->>ApplySpecialClass: Parse text with regex
ApplySpecialClass->>ApplySpecialClass: Match code, links, channels
alt Markdown Link Found
ApplySpecialClass->>LinkPreviewCard: Render with href
User->>LinkPreviewCard: Mouse enter (150ms delay)
LinkPreviewCard->>LinkPreviewCard: Check cache
alt Cache Miss
LinkPreviewCard->>API: GET /api/link-preview?url=...
API->>External: Fetch HTML (5s timeout)
External-->>API: Return HTML
API->>API: Extract og:title, og:description, og:image
API->>API: Decode HTML entities
API-->>LinkPreviewCard: Return metadata + cache header
LinkPreviewCard->>LinkPreviewCard: Store in cache
end
LinkPreviewCard->>User: Display preview card with animation
User->>LinkPreviewCard: Mouse leave
LinkPreviewCard->>LinkPreviewCard: 200ms delay
LinkPreviewCard->>User: Hide card with animation
end
alt Inline Code Found
ApplySpecialClass->>User: Render styled code element
end
alt Channel Mention Found
ApplySpecialClass->>User: Render styled channel link
end
🎯 Key Changes:
📊 Impact Assessment:
⚙️ SettingsSeverity Threshold: 📖 User Guide
|
|
Note Linting checks passed successfully 🎉 All formatting and code quality checks are clean. You're good to merge 🚀 |
| } else if (link) { | ||
| const linkMatch = link.match(/^\[([^\]]+)\]\(([^)]+)\)$/); | ||
| if (linkMatch) { | ||
| parts.push( | ||
| <LinkPreviewCard key={key++} href={linkMatch[2]}> | ||
| {linkMatch[1]} | ||
| </LinkPreviewCard>, | ||
| ); | ||
| } else { | ||
| parts.push(full); | ||
| } | ||
| } else if (channel) { | ||
| parts.push( | ||
| <span |
There was a problem hiding this comment.
The link parsing logic doesn't validate URL schemes, which creates an XSS vulnerability. Malicious content could inject javascript: URLs that execute arbitrary code when clicked.
For example, a malicious page content like [Click me](javascript:alert('XSS')) would be rendered as a clickable link that executes JavaScript.
Confidence: 5/5
Suggested Fix
| } else if (link) { | |
| const linkMatch = link.match(/^\[([^\]]+)\]\(([^)]+)\)$/); | |
| if (linkMatch) { | |
| parts.push( | |
| <LinkPreviewCard key={key++} href={linkMatch[2]}> | |
| {linkMatch[1]} | |
| </LinkPreviewCard>, | |
| ); | |
| } else { | |
| parts.push(full); | |
| } | |
| } else if (channel) { | |
| parts.push( | |
| <span | |
| const linkMatch = link.match(/^\[([^\]]+)\]\(([^)]+)\)$/); | |
| if (linkMatch) { | |
| const url = linkMatch[2]; | |
| const isValidUrl = /^(https?:\/\/|\/|#)/.test(url); | |
| if (isValidUrl) { | |
| parts.push( | |
| <a | |
| key={key++} | |
| href={linkMatch[2]} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="hover:underline cursor-pointer hover:opacity-80 transition-opacity" | |
| style={{ color: "#a5b4fc" }} | |
| > | |
| {linkMatch[1]} | |
| </a>, | |
| ); | |
| } else { | |
| parts.push(full); | |
| } | |
Add URL scheme validation to only allow safe protocols (https://, http://, relative paths /, or anchors #). This prevents javascript:, data:, and other potentially dangerous URL schemes from being rendered as clickable links.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In app/pages/[slug]/PageClient.tsx around line 54-67, the link parsing logic in the ApplySpecialClass component doesn't validate URL schemes before rendering them in href attributes, creating an XSS vulnerability where malicious content could inject javascript: URLs; add URL scheme validation using a regex like /^(https?:\/\/|\/|#)/.test(url) to only allow http://, https://, relative paths, and anchor links, and render the original text if the URL scheme is invalid.
📍 This suggestion applies to lines 54-67
| try { | ||
| new URL(url); |
There was a problem hiding this comment.
The URL validation only checks if the URL is parseable, but doesn't prevent Server-Side Request Forgery (SSRF) attacks. Attackers can use this endpoint to access internal network resources, cloud metadata endpoints (e.g., http://169.254.169.254/latest/meta-data/), or perform port scanning of internal infrastructure.
For example:
?url=http://localhost:6379- Access internal Redis?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/- AWS metadata?url=http://192.168.1.1/admin- Internal network resources
Confidence: 5/5
Suggested Fix
| try { | |
| new URL(url); | |
| try { | |
| const parsedUrl = new URL(url); | |
| // Block dangerous protocols | |
| if (!['http:', 'https:'].includes(parsedUrl.protocol)) { | |
| return NextResponse.json({ error: "invalid protocol" }, { status: 400 }); | |
| } | |
| // Block private IP ranges and localhost | |
| const hostname = parsedUrl.hostname.toLowerCase(); | |
| const privatePatterns = [ | |
| /^localhost$/i, | |
| /^127\./, | |
| /^10\./, | |
| /^172\.(1[6-9]|2[0-9]|3[0-1])\./, | |
| /^192\.168\./, | |
| /^169\.254\./, // AWS metadata | |
| /^::1$/, // IPv6 localhost | |
| /^fc00:/, // IPv6 private | |
| /^fe80:/, // IPv6 link-local | |
| ]; | |
| if (privatePatterns.some(pattern => pattern.test(hostname))) { | |
| return NextResponse.json({ error: "private urls not allowed" }, { status: 400 }); | |
| } | |
| } catch { | |
| return NextResponse.json({ error: "invalid url" }, { status: 400 }); | |
| } | |
Add comprehensive SSRF protection by validating the URL protocol and blocking private IP ranges, localhost, and cloud metadata endpoints. This prevents attackers from using your server to access internal resources.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In app/api/link-preview/route.ts around line 45-46, the URL validation only checks if the URL is parseable but doesn't prevent SSRF attacks where attackers can access internal network resources, cloud metadata endpoints, or perform port scanning; add comprehensive validation to block dangerous protocols (only allow http/https), private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16), localhost, and IPv6 private addresses before allowing the fetch operation.
📍 This suggestion applies to lines 45-46
| return NextResponse.json({ error: "failed to fetch" }, { status: 502 }); | ||
| } | ||
|
|
||
| const html = await res.text(); |
There was a problem hiding this comment.
The res.text() call has no size limit, allowing attackers to exhaust server memory by providing URLs that return extremely large responses (e.g., multi-gigabyte files). This can cause out-of-memory crashes and denial of service.
Confidence: 5/5
Suggested Fix
| const html = await res.text(); | |
| const contentLength = res.headers.get('content-length'); | |
| if (contentLength && parseInt(contentLength) > 1024 * 1024) { | |
| return NextResponse.json({ error: "response too large" }, { status: 502 }); | |
| } | |
| const html = await res.text(); | |
| if (html.length > 1024 * 1024) { | |
| return NextResponse.json({ error: "response too large" }, { status: 502 }); | |
| } | |
Add response size validation before and after reading the body. Check the Content-Length header if available, and verify the actual text length doesn't exceed 1MB. This prevents memory exhaustion attacks.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In app/api/link-preview/route.ts around line 69, the res.text() call has no size limit which allows attackers to exhaust server memory by providing URLs that return extremely large responses; add validation to check the Content-Length header before reading (if available) and verify the actual text length after reading, rejecting responses larger than 1MB to prevent memory exhaustion and DoS attacks.
| const res = await fetch(url, { | ||
| signal: controller.signal, | ||
| headers: { | ||
| "User-Agent": | ||
| "Mozilla/5.0 (compatible; LinkPreviewBot/1.0; +https://example.com/bot)", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
The fetch request doesn't follow redirects safely and could be exploited for SSRF bypass. An attacker could use a public URL that redirects to an internal resource (e.g., a URL shortener pointing to http://localhost), bypassing initial URL validation.
Confidence: 5/5
Suggested Fix
Add redirect: 'manual' to the fetch options and validate redirect URLs before following them, or use redirect: 'error' to block all redirects:
| const res = await fetch(url, { | |
| signal: controller.signal, | |
| headers: { | |
| "User-Agent": | |
| "Mozilla/5.0 (compatible; LinkPreviewBot/1.0; +https://example.com/bot)", | |
| }, | |
| }); | |
| const res = await fetch(url, { | |
| signal: controller.signal, | |
| redirect: 'error', | |
| headers: { | |
| "User-Agent": | |
| "Mozilla/5.0 (compatible; LinkPreviewBot/1.0; +https://example.com/bot)", | |
| }, | |
| }); | |
Setting redirect: 'error' prevents the fetch from following redirects, which blocks SSRF bypass attacks where an attacker uses a public URL that redirects to internal resources. If you need to support redirects, use redirect: 'manual' and validate each redirect URL against the same SSRF protection rules.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In app/api/link-preview/route.ts around line 55-61, the fetch request doesn't prevent redirect-based SSRF bypass where attackers use public URLs that redirect to internal resources; add redirect: 'error' to the fetch options to block all redirects, or if redirects are needed, use redirect: 'manual' and validate each redirect URL against the same SSRF protection rules before following it.
📍 This suggestion applies to lines 55-61
| url: string; | ||
| } | ||
|
|
||
| const previewCache = new Map<string, PreviewData | null>(); |
There was a problem hiding this comment.
The global previewCache Map is never cleared and will grow indefinitely as users hover over different links throughout their session. In a long-running application, this can cause memory leaks as the cache accumulates preview data for every unique URL encountered.
Confidence: 5/5
Suggested Fix
Implement a cache size limit or LRU (Least Recently Used) eviction strategy:
| const previewCache = new Map<string, PreviewData | null>(); | |
| const MAX_CACHE_SIZE = 100; | |
| const previewCache = new Map<string, PreviewData | null>(); | |
| function addToCache(key: string, value: PreviewData | null) { | |
| if (previewCache.size >= MAX_CACHE_SIZE) { | |
| const firstKey = previewCache.keys().next().value; | |
| previewCache.delete(firstKey); | |
| } | |
| previewCache.set(key, value); | |
| } | |
Then update line 61 and 65 to use addToCache(href, json) and addToCache(href, null) instead of direct previewCache.set() calls. This prevents unbounded memory growth by limiting the cache to 100 entries.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In components/LinkPreviewCard.tsx at line 12, the global previewCache Map is never cleared and will grow indefinitely causing memory leaks in long-running sessions; implement a cache size limit (e.g., MAX_CACHE_SIZE = 100) with LRU eviction by creating a helper function that removes the oldest entry when the cache exceeds the limit, and update lines 61 and 65 to use this helper instead of direct Map.set() calls.
| <div className="relative"> | ||
| {data?.image && !imgError && ( | ||
| <img | ||
| src={data.image} |
There was a problem hiding this comment.
The image src attribute uses data.image directly without validating the URL scheme. If the API endpoint is compromised or returns malicious data, this could render javascript: or data: URLs that execute arbitrary code when the image fails to load and triggers error handlers, or through other browser-specific vulnerabilities.
Confidence: 5/5
Suggested Fix
| src={data.image} | |
| src={data.image.startsWith('http://') || data.image.startsWith('https://') ? data.image : ''} | |
Validate that the image URL uses a safe protocol (http:// or https://) before rendering it. This prevents javascript:, data:, and other potentially dangerous URL schemes from being used in the src attribute.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In components/LinkPreviewCard.tsx at line 174, the image src attribute uses data.image directly without URL scheme validation, which could allow javascript: or data: URLs if the API is compromised; add validation to only allow http:// or https:// URLs by checking data.image.startsWith('http://') || data.image.startsWith('https://') before rendering, otherwise use an empty string.
Summary by BeetleThis PR enhances the security and robustness of the link preview functionality by implementing comprehensive safeguards against SSRF (Server-Side Request Forgery) attacks and resource exhaustion. The changes prevent the API from being exploited to access internal network resources, enforce response size limits, and improve the overall security posture of the link preview feature. Additionally, the PR includes minor refactoring to reorganize component imports and adds CORS headers for controlled external access. 📁 File Changes Summary (Consolidated across all commits):
Total Changes: 7 files changed, +84 additions, -13 deletions 🗺️ Walkthrough:sequenceDiagram
participant Client
participant API as /api/link-preview
participant Validator as URL Validator
participant External as External Site
Client->>API: GET /api/link-preview?url=...
API->>Validator: isSafeUrl(url)
Note over Validator: Check protocol (http/https only)
Validator->>Validator: Parse hostname
Note over Validator: Block private IPs: localhost, 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, IPv6 private
alt URL is unsafe
Validator-->>API: false
API-->>Client: 400 "invalid or disallowed url"
else URL is safe
Validator-->>API: true
API->>External: fetch(url, {redirect: "error"})
alt Response too large
External-->>API: Content-Length > 1MB
API-->>Client: 502 "response too large"
else Response OK
External-->>API: HTML content
API->>API: Validate HTML size < 1MB
API->>API: Extract metadata
API->>Validator: isSafeUrl(imageUrl)
alt Image URL safe
Validator-->>API: true
API-->>Client: 200 {title, description, image}
else Image URL unsafe
Validator-->>API: false
API-->>Client: 200 {title, description, image: null}
end
🎯 Key Changes:
📊 Impact Assessment:
⚙️ SettingsSeverity Threshold: 📖 User Guide
|
| const PRIVATE_HOSTNAME_PATTERNS = [ | ||
| /^localhost$/i, | ||
| /^0\.0\.0\.0$/, | ||
| /^127\./, | ||
| /^10\./, | ||
| /^172\.(1[6-9]|2[0-9]|3[0-1])\./, | ||
| /^192\.168\./, | ||
| /^169\.254\./, | ||
| /^::1$/, | ||
| /^fc00:/i, | ||
| /^fe80:/i, | ||
| ]; |
There was a problem hiding this comment.
The SSRF protection is missing several critical private IP ranges that could be exploited:
- Missing 100.64.0.0/10 (Carrier-grade NAT - RFC 6598)
- Missing 192.0.0.0/24 (IETF Protocol Assignments)
- Missing 192.0.2.0/24 (TEST-NET-1)
- Missing 198.18.0.0/15 (Benchmark testing)
- Missing 198.51.100.0/24 (TEST-NET-2)
- Missing 203.0.113.0/24 (TEST-NET-3)
- Missing 224.0.0.0/4 (Multicast)
- Missing 240.0.0.0/4 (Reserved)
- Missing IPv6 loopback range (::1 is covered, but not the full ::1/128)
- Missing IPv6 unique local addresses (fd00::/8)
An attacker could potentially use these ranges to bypass SSRF protection and access internal services.
Confidence: 5/5
Suggested Fix
| const PRIVATE_HOSTNAME_PATTERNS = [ | |
| /^localhost$/i, | |
| /^0\.0\.0\.0$/, | |
| /^127\./, | |
| /^10\./, | |
| /^172\.(1[6-9]|2[0-9]|3[0-1])\./, | |
| /^192\.168\./, | |
| /^169\.254\./, | |
| /^::1$/, | |
| /^fc00:/i, | |
| /^fe80:/i, | |
| ]; | |
| const PRIVATE_HOSTNAME_PATTERNS = [ | |
| /^localhost$/i, | |
| /^0\.0\.0\.0$/, | |
| /^127\./, | |
| /^10\./, | |
| /^172\.(1[6-9]|2[0-9]|3[0-1])\./, | |
| /^192\.168\./, | |
| /^169\.254\./, | |
| /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./, | |
| /^192\.0\.0\./, | |
| /^192\.0\.2\./, | |
| /^198\.18\./, | |
| /^198\.19\./, | |
| /^198\.51\.100\./, | |
| /^203\.0\.113\./, | |
| /^22[4-9]\./, | |
| /^23[0-9]\./, | |
| /^24[0-9]\./, | |
| /^25[0-5]\./, | |
| /^::1$/, | |
| /^::ffff:127\./i, | |
| /^fc00:/i, | |
| /^fd00:/i, | |
| /^fe80:/i, | |
| ]; | |
Add comprehensive private IP range coverage including carrier-grade NAT (100.64.0.0/10), test networks, multicast ranges (224.0.0.0/4), reserved ranges (240.0.0.0/4), and IPv6 unique local addresses (fd00::/8). This prevents attackers from exploiting gaps in SSRF protection to access internal services.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In app/api/link-preview/route.ts around line 37, the PRIVATE_HOSTNAME_PATTERNS array is missing several critical private IP ranges that could allow SSRF attacks to bypass protection; add patterns for 100.64.0.0/10 (carrier-grade NAT), 192.0.0.0/24, 192.0.2.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24 (test networks), 224.0.0.0/4 (multicast), 240.0.0.0/4 (reserved), ::ffff:127.0.0.0/104 (IPv4-mapped IPv6), and fd00::/8 (IPv6 unique local addresses) to ensure comprehensive SSRF protection.
📍 This suggestion applies to lines 37-48
|
Note Linting checks passed successfully 🎉 All formatting and code quality checks are clean. You're good to merge 🚀 |
| "key": "Access-Control-Allow-Headers", | ||
| "value": "Content-Type" |
There was a problem hiding this comment.
The CORS configuration is missing the Access-Control-Allow-Credentials header. Without explicitly setting this header, the browser's default behavior may vary, and if credentials are ever needed in the future, this could lead to security vulnerabilities. More critically, the current configuration doesn't include Access-Control-Max-Age, which means the browser will send a preflight OPTIONS request for every single API call, significantly impacting performance.
Additionally, for a public link preview API that's being called from a specific origin, you should consider whether you actually need CORS at all, or if this should be a server-side only API call to prevent abuse.
Confidence: 5/5
Suggested Fix
| "key": "Access-Control-Allow-Headers", | |
| "value": "Content-Type" | |
| { | |
| "key": "Access-Control-Allow-Headers", | |
| "value": "Content-Type" | |
| }, | |
| { | |
| "key": "Access-Control-Max-Age", | |
| "value": "86400" | |
| }, | |
Add Access-Control-Max-Age header with a value of 86400 (24 hours) to cache preflight responses and reduce unnecessary OPTIONS requests. This significantly improves performance by preventing the browser from sending a preflight request for every API call. Consider also explicitly setting Access-Control-Allow-Credentials: false if credentials are not needed, making the security posture explicit.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In vercel.json around line 15, the CORS configuration is missing the Access-Control-Max-Age header which causes the browser to send a preflight OPTIONS request for every API call, significantly impacting performance; add a new header object after the Access-Control-Allow-Headers configuration with key "Access-Control-Max-Age" and value "86400" (24 hours) to cache preflight responses, and consider adding "Access-Control-Allow-Credentials" with value "false" to make the security posture explicit.
📍 This suggestion applies to lines 15-16
| "key": "Access-Control-Allow-Origin", | ||
| "value": "https://devhub.vercel.app" |
There was a problem hiding this comment.
The Access-Control-Allow-Origin is hardcoded to a single production domain. This configuration will break local development and any staging/preview environments. When developers try to test the link preview functionality locally (e.g., http://localhost:3000), all API calls will fail due to CORS errors. This is a common misconfiguration that blocks development workflow.
Confidence: 4/5
Suggested Fix
Consider using environment-based configuration or allowing multiple origins. Since Vercel doesn't support environment variables in vercel.json, you have a few options:
- Handle CORS in the API route itself (recommended): Remove this configuration from
vercel.jsonand handle CORS dynamically inapp/api/link-preview/route.tswhere you can check the origin against an allowlist that includes localhost for development. - Use multiple Vercel configurations: Create separate
vercel.jsonfiles for different environments. - Allow multiple origins: If you control all the domains, list them explicitly (though Vercel's headers config doesn't support arrays, so option 1 is better).
The recommended approach is to remove CORS fromvercel.jsonand handle it in the API route where you have more control and can use environment variables.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
In vercel.json at line 7-8, the Access-Control-Allow-Origin is hardcoded to production domain which will break local development and staging environments; consider removing the CORS configuration from vercel.json and instead handling it dynamically in app/api/link-preview/route.ts where you can check the request origin against an environment-based allowlist that includes localhost for development, production domain for production, and preview URLs for staging.
📍 This suggestion applies to lines 7-8