-
Notifications
You must be signed in to change notification settings - Fork 78
Create a Mini Apps section #65
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
Conversation
|
Can we make the folder "mini-apps" instead of "miniapps" so that it's parallel to how we structure our urls on docs.base.org? |
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| notificationId: crypto.randomUUID(), | ||
| title, | ||
| body, | ||
| targetUrl: appUrl, | ||
| tokens: [notificationDetails.token], | ||
| } satisfies SendNotificationRequest), | ||
| }); |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
URL
user-provided value
The
URL
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix the SSRF vulnerability, the outgoing request's destination should never be based on arbitrary user-provided values. Instead, restrict the possible URLs by enforcing an allowlist of hostnames or base URLs that are permissible for notification delivery.
General fix:
Before using notificationDetails.url, validate it against a set of permitted endpoints (ideally using a list of allowed hostnames or base URLs). If the user-supplied URL is not valid or on the allowlist, reject the request or refuse to send the notification.
Best fix for this code:
- In
sendFrameNotification, before making the request tonotificationDetails.url, parse the URL and check that its hostname matches one of a strict set (e.g., set a constant list likeALLOWED_NOTIFICATION_HOSTNAMES). - If the check fails, return an error state.
- Add a helper function to do this validation.
- Add any necessary imports (e.g., for
URLglobal if needed, which is available in node & browsers).
Where to change:
- All changes are required in
mini-apps/workshops/mini-neynar/lib/notification-client.ts. - Insert a new constant allowlist and validation helper.
- Validate
notificationDetails.urlimmediately before the fetch.
-
Copy modified lines R10-R23 -
Copy modified lines R51-R61
| @@ -7,6 +7,20 @@ | ||
|
|
||
| const appUrl = process.env.NEXT_PUBLIC_URL || ""; | ||
|
|
||
| // SSRF prevention: restrict outgoing notification URL hostnames | ||
| const ALLOWED_NOTIFICATION_HOSTNAMES = [ | ||
| "api.neynar.com", // adapt as needed (example) | ||
| "notification.farcaster.xyz", // adapt as needed (example) | ||
| ]; | ||
|
|
||
| function isAllowedNotificationUrl(urlString: string): boolean { | ||
| try { | ||
| const url = new URL(urlString); | ||
| return ALLOWED_NOTIFICATION_HOSTNAMES.includes(url.hostname); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| type SendFrameNotificationResult = | ||
| | { | ||
| state: "error"; | ||
| @@ -34,6 +48,17 @@ | ||
| return { state: "no_token" }; | ||
| } | ||
|
|
||
| // SSRF prevention: validate notificationDetails.url | ||
| if ( | ||
| !notificationDetails.url || | ||
| !isAllowedNotificationUrl(notificationDetails.url) | ||
| ) { | ||
| return { | ||
| state: "error", | ||
| error: "Notification URL is invalid or not allowed", | ||
| }; | ||
| } | ||
|
|
||
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { |
| <div key={i} style={{ flex: 1, display: 'flex' }}> | ||
| {imageUrls[i] ? ( | ||
| <img | ||
| src={imageUrls[i]} |
Check warning
Code scanning / CodeQL
Client-side URL redirect Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix this, we should strictly validate or whitelist image URLs before using them in the OG image response. A robust strategy is to only accept image URLs from trusted domains (e.g. IPFS gateways, or your own asset bucket), and reject or ignore others. This prevents SSRF, abusive links, and poisoning. The fix involves modifying how imageUrls is constructed to filter only URLs matching allowed domains—for example, using a regular expression or URL API check. All changes can be made within the current file, within the function GET. No changes are necessary to the React DOM except for how imageUrls is constructed. No additional dependencies are required unless we want to use an external validator (but the standard URL class suffices).
-
Copy modified lines R16-R31 -
Copy modified line R38
| @@ -13,13 +13,29 @@ | ||
| const displayName = searchParams.get('displayName') || 'name'; | ||
|
|
||
| // Get token images from query params | ||
| // Validate URLs: Only allow image URLs from trusted domains | ||
| const TRUSTED_IMAGE_DOMAINS = [ | ||
| 'ipfs.io', | ||
| 'gateway.pinata.cloud', | ||
| 'zora.co', | ||
| // add more trusted domains as needed | ||
| ]; | ||
| function isTrustedImageUrl(url?: string) { | ||
| if (!url) return false; | ||
| try { | ||
| const parsed = new URL(url); | ||
| return TRUSTED_IMAGE_DOMAINS.some(domain => parsed.hostname.endsWith(domain)); | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| } | ||
| const imageUrls = [ | ||
| searchParams.get('img1'), | ||
| searchParams.get('img2'), | ||
| searchParams.get('img3'), | ||
| searchParams.get('img4'), | ||
| searchParams.get('img5'), | ||
| ].filter(Boolean); | ||
| ].map(url => (isTrustedImageUrl(url) ? url : undefined)); | ||
|
|
||
| console.log('🖼️ [OG-COLLAGE] Generating for:', displayName, 'with', imageUrls.length, 'images'); | ||
|
|
| <div key={i} style={{ flex: 1, display: 'flex' }}> | ||
| {imageUrls[i] ? ( | ||
| <img | ||
| src={imageUrls[i]} |
Check failure
Code scanning / CodeQL
Client-side cross-site scripting High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix this issue, we must ensure that the values in imageUrls are safe and valid image URLs before using them as the src attribute in any <img> tag. In practice, this means:
- Only accepting URLs that start with a scheme (e.g.
https://) and optionally, filtering for domains you trust (if possible). - Rejecting/ignoring any URL that starts with
javascript:,data:, or any other dangerous or non-image-providing protocol. - (Optionally) Only allow HTTPS, to avoid mixed content or other issues.
- The actual filter/validation must be performed at the point we construct
imageUrls, or just before we render the<img>.
Best Practice Fix:
Introduce a sanitization function that checks (a) the URL scheme, and (b) the host (if needed), to ensure only safe image URLs are rendered. Update the code where imageUrls is populated to filter or replace invalid values. For simplicity, let's allow only HTTPS URLs (excluding HTTP, data:, etc).
Implementation steps:
- Add a
sanitizeImageUrlfunction that accepts a string, tries to parse it as a URL, and returns the value only if it's a valid HTTPS url. - In the section where we build the
imageUrlsarray, apply this sanitizer. - If an image url is invalid or not present, the corresponding slot will remain
undefinedor not pass the filter, so a placeholder is rendered as before. - No external libraries are needed—built-in
URLsuffices.
All changes are confined to mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx.
-
Copy modified lines R15-R30 -
Copy modified lines R32-R37
| @@ -12,14 +12,29 @@ | ||
| const { searchParams } = new URL(request.url); | ||
| const displayName = searchParams.get('displayName') || 'name'; | ||
|
|
||
| // Get token images from query params | ||
| // Helper to allow only HTTPS image URLs | ||
| function sanitizeImageUrl(url: string | null): string | undefined { | ||
| if (!url) return undefined; | ||
| try { | ||
| const parsed = new URL(url); | ||
| if (parsed.protocol === 'https:') { | ||
| return url; | ||
| } | ||
| // else | ||
| return undefined; | ||
| } catch (e) { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| // Get token images from query params, only passing safe URLs | ||
| const imageUrls = [ | ||
| searchParams.get('img1'), | ||
| searchParams.get('img2'), | ||
| searchParams.get('img3'), | ||
| searchParams.get('img4'), | ||
| searchParams.get('img5'), | ||
| ].filter(Boolean); | ||
| sanitizeImageUrl(searchParams.get('img1')), | ||
| sanitizeImageUrl(searchParams.get('img2')), | ||
| sanitizeImageUrl(searchParams.get('img3')), | ||
| sanitizeImageUrl(searchParams.get('img4')), | ||
| sanitizeImageUrl(searchParams.get('img5')), | ||
| ]; | ||
|
|
||
| console.log('🖼️ [OG-COLLAGE] Generating for:', displayName, 'with', imageUrls.length, 'images'); | ||
|
|
| <div style={{ flex: 1, display: 'flex' }}> | ||
| {imageUrls[3] ? ( | ||
| <img | ||
| src={imageUrls[3]} |
Check warning
Code scanning / CodeQL
Client-side URL redirect Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
The best way to fix this issue is to validate each image URL provided by the user (the values of img1 ... img5). Rather than allowing any arbitrary URL, only allow image URLs where the domain matches a list of trusted hostnames (a whitelist), or even require that the URLs are relative paths or from an allowed CDN. This can be done by iterating through imageUrls and filtering out any images whose URLs do not match the allowed criteria. For maximum security and minimal disruption, add a simple domain whitelist check (for example, allow only images.zora.co, or another trusted set) before rendering. Alternatively, restrict to only allow certain patterns if images are always served from a known CDN.
Required changes:
- Add a list of allowed hostnames (or a function to validate image URLs).
- Update the construction of
imageUrlsto filter out any URLs not matching the allowed hosts/patterns. - Optionally, fallback to displaying a placeholder if the URL is invalid.
All changes are local to the code in mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx near where imageUrls is constructed.
-
Copy modified lines R15-R32 -
Copy modified line R39
| @@ -12,14 +12,31 @@ | ||
| const { searchParams } = new URL(request.url); | ||
| const displayName = searchParams.get('displayName') || 'name'; | ||
|
|
||
| // Get token images from query params | ||
| // Get token images from query params, and validate allowed domains | ||
| const ALLOWED_IMAGE_HOSTS = [ | ||
| 'images.zora.co', // example trusted CDN -- replace or expand as appropriate | ||
| // Add other trusted domains as needed. | ||
| ]; | ||
| function isValidImageUrl(url: string | null): url is string { | ||
| if (!url) return false; | ||
| try { | ||
| const u = new URL(url, 'https://zora.co'); // relative URLs resolve to trusted domain | ||
| // Only allow if host matches a trusted host, and protocol is http or https | ||
| return ( | ||
| (u.protocol === 'https:' || u.protocol === 'http:') && | ||
| ALLOWED_IMAGE_HOSTS.includes(u.host) | ||
| ); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| const imageUrls = [ | ||
| searchParams.get('img1'), | ||
| searchParams.get('img2'), | ||
| searchParams.get('img3'), | ||
| searchParams.get('img4'), | ||
| searchParams.get('img5'), | ||
| ].filter(Boolean); | ||
| ].filter(isValidImageUrl); | ||
|
|
||
| console.log('🖼️ [OG-COLLAGE] Generating for:', displayName, 'with', imageUrls.length, 'images'); | ||
|
|
| <div style={{ flex: 1, display: 'flex' }}> | ||
| {imageUrls[3] ? ( | ||
| <img | ||
| src={imageUrls[3]} |
Check failure
Code scanning / CodeQL
Client-side cross-site scripting High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To correct this vulnerability, all user-supplied image URLs should be validated before use. The validation should ensure that each image URL:
- Is a valid absolute HTTPS (or HTTP if strictly needed) URL.
- Does not use potentially dangerous schemes (
javascript:,data:, etc.). - Is not blank, undefined, or malformed.
This should be done for every image URL extracted from query parameters (img1...img5). The best approach in TypeScript is to use the built-in URL constructor for validation. We can create a helper function, e.g., isSafeImageUrl, and use it to filter out unsafe URLs when constructing imageUrls. In the returned JSX, only validated safe URLs will be embedded in the <img src="..."> attributes.
Changes are required in the block constructing imageUrls in GET, lines 16–22, to filter and validate URLs. No change is needed at the sink (line 94, etc.) except that it now only receives validated data. If a URL is invalid, it should not be added to the array, so the empty tile is used instead.
No external packages are required; only standard TypeScript and built-in APIs.
-
Copy modified lines R15-R30 -
Copy modified line R37
| @@ -12,14 +12,29 @@ | ||
| const { searchParams } = new URL(request.url); | ||
| const displayName = searchParams.get('displayName') || 'name'; | ||
|
|
||
| // Get token images from query params | ||
| // Image URL validation helper | ||
| function isSafeImageUrl(url: string | null): url is string { | ||
| if (!url) return false; | ||
| try { | ||
| const parsed = new URL(url); | ||
| if (!['http:', 'https:'].includes(parsed.protocol)) return false; | ||
| if (!parsed.hostname) return false; | ||
| // Optional: restrict to known image extensions | ||
| // if (!/\.(jpg|jpeg|png|webp|gif|svg)$/i.test(parsed.pathname)) return false; | ||
| return true; | ||
| } catch (_) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Get token images from query params, validating safety | ||
| const imageUrls = [ | ||
| searchParams.get('img1'), | ||
| searchParams.get('img2'), | ||
| searchParams.get('img3'), | ||
| searchParams.get('img4'), | ||
| searchParams.get('img5'), | ||
| ].filter(Boolean); | ||
| ].filter(isSafeImageUrl); | ||
|
|
||
| console.log('🖼️ [OG-COLLAGE] Generating for:', displayName, 'with', imageUrls.length, 'images'); | ||
|
|
| <div style={{ flex: 2, display: 'flex' }}> | ||
| {imageUrls[4] ? ( | ||
| <img | ||
| src={imageUrls[4]} |
Check warning
Code scanning / CodeQL
Client-side URL redirect Medium
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix the problem, we must ensure that the img1-img5 query parameters, which provide the src URLs for images, cannot be used to load arbitrary user-controlled URLs. The most robust fix is to only accept URLs that match an explicit whitelist (e.g., only your own domains, or same-origin URLs), or at minimum, to only allow well-formed HTTP(s) URLs and never allow things like data: or javascript: URIs.
Since the provided code is in the serverless/Edge runtime and the only code shown does not include any domain whitelist or URL validation, we must introduce basic sanitization for these URLs. The best approach would involve:
- Rejecting any URLs not starting with
https://(orhttp://if required), or only allowing image URLs from certain domains relevant to your service. - Optionally, ignoring or replacing any URL that does not validate, such as not assigning it to
srcor using a fallback image or a placeholder.
No changes are needed for existing functionality outside this; just introduce a helper function to check and filter allowed URLs and use this when constructing imageUrls.
All changes required will be in mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx:
- Add a helper function for URL validation (e.g., only allow HTTPS URLs, maybe restrict to known domains).
- Filter
imageUrlsthrough this sanitizer before using. - No new imports necessary;
URLandURLSearchParamsare built-ins.
-
Copy modified lines R6-R24 -
Copy modified line R34 -
Copy modified lines R36-R41
| @@ -3,6 +3,25 @@ | ||
|
|
||
| export const runtime = 'edge'; | ||
|
|
||
| // Helper: Only allow HTTPS URLs and block data/javascript URIs, optionally limit to a domain or return undefined | ||
| function sanitizeImageUrl(url: string | null): string | undefined { | ||
| if (!url) return undefined; | ||
| try { | ||
| const parsed = new URL(url, 'https://dummy.base/'); // allow relative | ||
| // Allowed schemes: only https and http | ||
| if ( | ||
| (parsed.protocol === 'https:' || parsed.protocol === 'http:') | ||
| // Optionally: restrict domain, e.g. | ||
| // && parsed.hostname.endsWith('.yourdomain.com') | ||
| ) { | ||
| return url; | ||
| } | ||
| } catch (e) { | ||
| // invalid URL | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Generate OG Image for Zora Collage using Vercel OG Image service | ||
| * This is server-side generation - more reliable than client-side screen capture | ||
| @@ -12,14 +31,14 @@ | ||
| const { searchParams } = new URL(request.url); | ||
| const displayName = searchParams.get('displayName') || 'name'; | ||
|
|
||
| // Get token images from query params | ||
| // Get token images from query params, sanitize URLs | ||
| const imageUrls = [ | ||
| searchParams.get('img1'), | ||
| searchParams.get('img2'), | ||
| searchParams.get('img3'), | ||
| searchParams.get('img4'), | ||
| searchParams.get('img5'), | ||
| ].filter(Boolean); | ||
| sanitizeImageUrl(searchParams.get('img1')), | ||
| sanitizeImageUrl(searchParams.get('img2')), | ||
| sanitizeImageUrl(searchParams.get('img3')), | ||
| sanitizeImageUrl(searchParams.get('img4')), | ||
| sanitizeImageUrl(searchParams.get('img5')), | ||
| ].filter(Boolean) as string[]; | ||
|
|
||
| console.log('🖼️ [OG-COLLAGE] Generating for:', displayName, 'with', imageUrls.length, 'images'); | ||
|
|
| <div style={{ flex: 2, display: 'flex' }}> | ||
| {imageUrls[4] ? ( | ||
| <img | ||
| src={imageUrls[4]} |
Check failure
Code scanning / CodeQL
Client-side cross-site scripting High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix this issue, we must ensure only safe and valid image URLs are used as the src attribute for rendered <img> tags. The best solution is to strictly validate and sanitize the image URLs. Specifically, we should permit only HTTP and HTTPS URLs and reject any other schemes (such as javascript: or data:). This validation needs to be performed on all user-supplied URLs (img1-5) when building imageUrls array, right after fetching them from searchParams.
To implement this:
- Add a helper function that takes a string and returns the URL only if it's HTTP(S) and externally safe, otherwise returns
undefined. - Apply this function to every input URL before including it in
imageUrls. - No framework-specific escaping is needed since with validation, the risk is removed.
- All changes are to be performed inside
mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsxwithin shown snippets.
-
Copy modified lines R15-R28 -
Copy modified lines R30-R34
| @@ -12,13 +12,26 @@ | ||
| const { searchParams } = new URL(request.url); | ||
| const displayName = searchParams.get('displayName') || 'name'; | ||
|
|
||
| // Get token images from query params | ||
| // Helper to validate image URLs | ||
| const isSafeImageUrl = (url: string | null): string | undefined => { | ||
| if (!url) return undefined; | ||
| // Only allow http/https URLs | ||
| try { | ||
| const parsed = new URL(url); | ||
| if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { | ||
| return url; | ||
| } | ||
| } catch (e) { /* Invalid URL */ } | ||
| return undefined; | ||
| }; | ||
|
|
||
| // Get token images from query params and validate | ||
| const imageUrls = [ | ||
| searchParams.get('img1'), | ||
| searchParams.get('img2'), | ||
| searchParams.get('img3'), | ||
| searchParams.get('img4'), | ||
| searchParams.get('img5'), | ||
| isSafeImageUrl(searchParams.get('img1')), | ||
| isSafeImageUrl(searchParams.get('img2')), | ||
| isSafeImageUrl(searchParams.get('img3')), | ||
| isSafeImageUrl(searchParams.get('img4')), | ||
| isSafeImageUrl(searchParams.get('img5')), | ||
| ].filter(Boolean); | ||
|
|
||
| console.log('🖼️ [OG-COLLAGE] Generating for:', displayName, 'with', imageUrls.length, 'images'); |
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| notificationId: crypto.randomUUID(), | ||
| title, | ||
| body, | ||
| targetUrl: appUrl, | ||
| tokens: [notificationDetails.token], | ||
| } satisfies SendNotificationRequest), | ||
| }); |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
URL
user-provided value
The
URL
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix this SSRF vulnerability, you must restrict the URLs used for outbound requests in sendFrameNotification. The best fix is to only allow requests to URLs matching a fixed allow-list (e.g., trusted notification services your app integrates with). You can do this by checking the hostname or origin of notificationDetails.url against an array of allowed hostnames. If the user-supplied URL does not match, reject the request with an error.
Specifically, in mini-apps/workshops/my-mini-zora/lib/notification-client.ts within the sendFrameNotification function:
- Before using
notificationDetails.urlinfetch(), parse it, check its hostname or origin against a trusted allow-list (which may be defined in environment variables or a static array). - If not allowed, return an error state, do not make the outbound request.
- This validation should only be applied if
notificationDetails.urlcomes from the untrusted (user-supplied) source, which, in the shown code, must always be checked.
You may need to import the Node.js URL class (for robust URL parsing), and define a static array or environment-based allow-list of trusted hosts.
-
Copy modified lines R8-R10 -
Copy modified lines R40-R53
| @@ -5,6 +5,9 @@ | ||
| } from "@farcaster/frame-sdk"; | ||
| import { getUserNotificationDetails } from "@/lib/notification"; | ||
|
|
||
| // Needed for robust URL validation in Node/Edge | ||
| // (global URL is available in most modern runtimes, otherwise use import) | ||
|
|
||
| const appUrl = process.env.NEXT_PUBLIC_URL || ""; | ||
|
|
||
| type SendFrameNotificationResult = | ||
| @@ -34,6 +37,20 @@ | ||
| return { state: "no_token" }; | ||
| } | ||
|
|
||
| // SSRF Fix: Validate notificationDetails.url | ||
| let notificationUrl: URL; | ||
| try { | ||
| notificationUrl = new URL(notificationDetails.url); | ||
| } catch (e) { | ||
| return { state: "error", error: "Invalid notification URL" }; | ||
| } | ||
| if (!ALLOWED_NOTIFICATION_HOSTS.includes(notificationUrl.hostname)) { | ||
| return { | ||
| state: "error", | ||
| error: `Notification host not allowed: ${notificationUrl.hostname}`, | ||
| }; | ||
| } | ||
|
|
||
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { |
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| notificationId: crypto.randomUUID(), | ||
| title, | ||
| body, | ||
| targetUrl: appUrl, | ||
| tokens: [notificationDetails.token], | ||
| } satisfies SendNotificationRequest), | ||
| }); |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
URL
user-provided value
The
URL
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
The best practice for fixing SSRF vulnerabilities is to never allow user input to dictate the destination host of an outgoing request directly. You should either:
- Only allow URLs from a hardcoded allowlist or determined by server logic, not from the request body;
- Or, strictly validate the URL, for example, enforcing that it belongs to a specific (publicly-routable) domain, or matches a whitelist/pattern, and rejecting anything else.
Here, since the API allows passing notificationDetails.url from the client, we must validate that the passed URL is "safe". A robust, but simple, solution:
- Check that the URL is http(s), belongs to an allowlisted hostname, or at minimum, is not an internal IP address, localhost, or a private network.
- Reject (error out) if the URL does not meet this criterion.
The most common approach is to parse the URL (with new URL()), check its protocol, and compare the hostname against an allowlist or block internal IP ranges (using regex or a library like is-valid-host or is-ip-private). As we should avoid changing existing functionality, but must secure the SSRF vector, we should block localhost, private, and internal (RFC1918) addresses, and only allow http/https URLs.
File/region changes:
- Add a validation step in
notification-client.tsbefore callingfetch(notificationDetails.url, ...). - The check will parse the URL, confirm the protocol is http/https, and block internal/private IPs and localhost domains.
- We can use a small helper for IP and hostname checks (since common libraries can't be assumed installed), or rely on simple regex or checking for common values (127.0.0.1, ::1, localhost, etc.).
-
Copy modified lines R8-R38 -
Copy modified lines R68-R72
| @@ -5,6 +5,37 @@ | ||
| } from "@farcaster/frame-sdk"; | ||
| import { getUserNotificationDetails } from "@/lib/notification"; | ||
|
|
||
| // SSRF mitigation: Check if URL is private/internal/localhost | ||
| function isPrivateOrLocalhost(urlString: string): boolean { | ||
| try { | ||
| const url = new URL(urlString); | ||
| // Disallow non-http(s) | ||
| if (!/^https?:$/.test(url.protocol)) return true; | ||
| const hostname = url.hostname; | ||
| // Disallow localhost, 127.0.0.1, ::1 | ||
| if ( | ||
| hostname === "localhost" || | ||
| hostname === "127.0.0.1" || | ||
| hostname === "::1" | ||
| ) { | ||
| return true; | ||
| } | ||
| // Disallow IPv4 private ranges | ||
| // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | ||
| if ( | ||
| /^10(\.\d{1,3}){3}$/.test(hostname) || | ||
| /^192\.168(\.\d{1,3}){2}$/.test(hostname) || | ||
| /^172\.(1[6-9]|2\d|3[0-1])(\.\d{1,3}){2}$/.test(hostname) | ||
| ) { | ||
| return true; | ||
| } | ||
| // Optionally, disallow link-local, loopback blocks, etc. | ||
| return false; | ||
| } catch { | ||
| return true; // treat parse errors as unsafe | ||
| } | ||
| } | ||
|
|
||
| const appUrl = process.env.NEXT_PUBLIC_URL || ""; | ||
|
|
||
| type SendFrameNotificationResult = | ||
| @@ -34,6 +65,11 @@ | ||
| return { state: "no_token" }; | ||
| } | ||
|
|
||
| // SSRF mitigation: Validate notificationDetails.url | ||
| if (isPrivateOrLocalhost(notificationDetails.url)) { | ||
| return { state: "error", error: "Invalid or unsafe notification target URL" }; | ||
| } | ||
|
|
||
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { |
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| notificationId: crypto.randomUUID(), | ||
| title, | ||
| body, | ||
| targetUrl: appUrl, | ||
| tokens: [notificationDetails.token], | ||
| } satisfies SendNotificationRequest), | ||
| }); |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
URL
user-provided value
The
URL
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
To fix this SSRF vulnerability, user input must not be used directly as the target of an outgoing HTTP request. Instead, the hostname component (at minimum) should be strongly validated against an explicit allow-list of safe endpoints. A safe and effective approach is to maintain a server-side allow-list of valid base URLs for notifications, and require that any user input be mapped to one of these allowable values (for example, clients can supply an ID or a name, not the full URL). Alternatively, if some user-supplied information must be included, parse the URL and strictly validate or reject unacceptable hostnames.
Concretely, in sendFrameNotification, before calling fetch(notificationDetails.url, ...), you should check that notificationDetails.url matches a list of permitted URLs (or hostnames), or at least that its hostname is in a known allow-list. This allow-list can be a list of valid strings defined in this file. If the value is unacceptable, return an error rather than proceeding with the request.
To do this:
- Add a helper function to validate that the provided
notificationDetails.urlis in the allow-list (matching hostnames or full URLs as appropriate). - Before fetching, parse and validate the URL. If validation fails, return an error.
- Optionally, update the type for
notificationDetailsor its usage documentation to clarify the URL is picked from a strict set. - Import the standard
URLclass if needed.
Only the mini-apps/workshops/three-card-monte/lib/notification-client.ts file is affected.
-
Copy modified line R7 -
Copy modified lines R9-R15 -
Copy modified lines R44-R54
| @@ -4,7 +4,15 @@ | ||
| sendNotificationResponseSchema, | ||
| } from "@farcaster/frame-sdk"; | ||
| import { getUserNotificationDetails } from "@/lib/notification"; | ||
| // No need for explicit URL import in modern Node/Edge runtimes (global) | ||
|
|
||
|
|
||
| // Allow-list of permitted notification service hostnames. | ||
| const ALLOW_LISTED_NOTIFICATION_HOSTNAMES = [ | ||
| "notify.myapp.com", // <-- Replace with real allowed hostnames | ||
| "another-allowed-service.com", | ||
| ]; | ||
|
|
||
| const appUrl = process.env.NEXT_PUBLIC_URL || ""; | ||
|
|
||
| type SendFrameNotificationResult = | ||
| @@ -34,6 +41,17 @@ | ||
| return { state: "no_token" }; | ||
| } | ||
|
|
||
| // Validate that the URL is in the host allow-list | ||
| let notificationUrl: URL; | ||
| try { | ||
| notificationUrl = new URL(notificationDetails.url); | ||
| } catch (e) { | ||
| return { state: "error", error: "Invalid notification URL format." }; | ||
| } | ||
| if (!ALLOW_LISTED_NOTIFICATION_HOSTNAMES.includes(notificationUrl.hostname)) { | ||
| return { state: "error", error: "Notification URL is not in the allow-list." }; | ||
| } | ||
|
|
||
| const response = await fetch(notificationDetails.url, { | ||
| method: "POST", | ||
| headers: { |
validate.txttool