Skip to content

Conversation

@hughescoin
Copy link
Contributor

  • Create a dedicated templates folder for Mini Apps that break down Farcaster SDK examples and Mini Kit examples.
  • Added a readme for the validate.txt tool
  • Included a MiniKit version for Dylan's full demo app

@ericbrown99
Copy link
Collaborator

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?

Comment on lines 37 to 49
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

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.

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 to notificationDetails.url, parse the URL and check that its hostname matches one of a strict set (e.g., set a constant list like ALLOWED_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 URL global 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.url immediately before the fetch.

Suggested changeset 1
mini-apps/workshops/mini-neynar/lib/notification-client.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/mini-neynar/lib/notification-client.ts b/mini-apps/workshops/mini-neynar/lib/notification-client.ts
--- a/mini-apps/workshops/mini-neynar/lib/notification-client.ts
+++ b/mini-apps/workshops/mini-neynar/lib/notification-client.ts
@@ -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: {
EOF
@@ -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: {
Copilot is powered by AI and may make mistakes. Always verify output.
<div key={i} style={{ flex: 1, display: 'flex' }}>
{imageUrls[i] ? (
<img
src={imageUrls[i]}

Check warning

Code scanning / CodeQL

Client-side URL redirect Medium

Untrusted URL redirection depends on a
user-provided value
.

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).

Suggested changeset 1
mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
--- a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
+++ b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
@@ -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');
 
EOF
@@ -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');

Copilot is powered by AI and may make mistakes. Always verify output.
<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

Cross-site scripting vulnerability due to
user-provided value
.

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 sanitizeImageUrl function 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 imageUrls array, apply this sanitizer.
  • If an image url is invalid or not present, the corresponding slot will remain undefined or not pass the filter, so a placeholder is rendered as before.
  • No external libraries are needed—built-in URL suffices.

All changes are confined to mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx.


Suggested changeset 1
mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
--- a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
+++ b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
@@ -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');
 
EOF
@@ -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');

Copilot is powered by AI and may make mistakes. Always verify output.
<div style={{ flex: 1, display: 'flex' }}>
{imageUrls[3] ? (
<img
src={imageUrls[3]}

Check warning

Code scanning / CodeQL

Client-side URL redirect Medium

Untrusted URL redirection depends on a
user-provided value
.

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:

  1. Add a list of allowed hostnames (or a function to validate image URLs).
  2. Update the construction of imageUrls to filter out any URLs not matching the allowed hosts/patterns.
  3. 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.


Suggested changeset 1
mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
--- a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
+++ b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
@@ -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');
 
EOF
@@ -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');

Copilot is powered by AI and may make mistakes. Always verify output.
<div style={{ flex: 1, display: 'flex' }}>
{imageUrls[3] ? (
<img
src={imageUrls[3]}

Check failure

Code scanning / CodeQL

Client-side cross-site scripting High

Cross-site scripting vulnerability due to
user-provided value
.

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.


Suggested changeset 1
mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
--- a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
+++ b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
@@ -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');
 
EOF
@@ -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');

Copilot is powered by AI and may make mistakes. Always verify output.
<div style={{ flex: 2, display: 'flex' }}>
{imageUrls[4] ? (
<img
src={imageUrls[4]}

Check warning

Code scanning / CodeQL

Client-side URL redirect Medium

Untrusted URL redirection depends on a
user-provided value
.

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:// (or http:// 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 src or 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 imageUrls through this sanitizer before using.
  • No new imports necessary; URL and URLSearchParams are built-ins.

Suggested changeset 1
mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
--- a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
+++ b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
@@ -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');
 
EOF
@@ -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');

Copilot is powered by AI and may make mistakes. Always verify output.
<div style={{ flex: 2, display: 'flex' }}>
{imageUrls[4] ? (
<img
src={imageUrls[4]}

Check failure

Code scanning / CodeQL

Client-side cross-site scripting High

Cross-site scripting vulnerability due to
user-provided value
.

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.tsx within shown snippets.
Suggested changeset 1
mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
--- a/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
+++ b/mini-apps/workshops/my-mini-zora/app/api/og-collage/route.tsx
@@ -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');
EOF
@@ -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');
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines 37 to 49
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

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.

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.url in fetch(), 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.url comes 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.


Suggested changeset 1
mini-apps/workshops/my-mini-zora/lib/notification-client.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-mini-zora/lib/notification-client.ts b/mini-apps/workshops/my-mini-zora/lib/notification-client.ts
--- a/mini-apps/workshops/my-mini-zora/lib/notification-client.ts
+++ b/mini-apps/workshops/my-mini-zora/lib/notification-client.ts
@@ -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: {
EOF
@@ -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: {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines 37 to 49
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

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.

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.ts before calling fetch(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.).

Suggested changeset 1
mini-apps/workshops/my-simple-mini-app/lib/notification-client.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/my-simple-mini-app/lib/notification-client.ts b/mini-apps/workshops/my-simple-mini-app/lib/notification-client.ts
--- a/mini-apps/workshops/my-simple-mini-app/lib/notification-client.ts
+++ b/mini-apps/workshops/my-simple-mini-app/lib/notification-client.ts
@@ -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: {
EOF
@@ -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: {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines 37 to 49
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

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.

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.url is 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 notificationDetails or its usage documentation to clarify the URL is picked from a strict set.
  • Import the standard URL class if needed.

Only the mini-apps/workshops/three-card-monte/lib/notification-client.ts file is affected.


Suggested changeset 1
mini-apps/workshops/three-card-monte/lib/notification-client.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/mini-apps/workshops/three-card-monte/lib/notification-client.ts b/mini-apps/workshops/three-card-monte/lib/notification-client.ts
--- a/mini-apps/workshops/three-card-monte/lib/notification-client.ts
+++ b/mini-apps/workshops/three-card-monte/lib/notification-client.ts
@@ -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: {
EOF
@@ -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: {
Copilot is powered by AI and may make mistakes. Always verify output.
@ericbrown99 ericbrown99 merged commit 61473de into master Sep 30, 2025
2 of 3 checks passed
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