diff --git a/apps/web/app/api/og/route.ts b/apps/web/app/api/og/route.ts
index e384c632..7753e6b5 100644
--- a/apps/web/app/api/og/route.ts
+++ b/apps/web/app/api/og/route.ts
@@ -19,7 +19,9 @@ function isPrivateHost(hostname: string): boolean {
if (
lowerHost === "localhost" ||
lowerHost === "127.0.0.1" ||
+ lowerHost === "0.0.0.0" ||
lowerHost === "::1" ||
+ lowerHost === "::" ||
lowerHost.startsWith("127.") ||
lowerHost.startsWith("0.0.0.0")
) {
@@ -30,6 +32,7 @@ function isPrivateHost(hostname: string): boolean {
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
+ /^169\.254\./, // Link-local / Metadata service
]
return privateIpPatterns.some((pattern) => pattern.test(hostname))
@@ -179,61 +182,70 @@ export async function GET(request: Request) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 8000)
- const response = await fetch(trimmedUrl, {
- signal: controller.signal,
- headers: {
- "User-Agent":
- "Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
- },
- })
+ try {
+ const response = await fetch(trimmedUrl, {
+ signal: controller.signal,
+ redirect: "manual",
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
+ },
+ })
- clearTimeout(timeoutId)
+ if (response.status >= 300 && response.status < 400) {
+ const location = response.headers.get("location")
+ if (location) {
+ const redirectUrl = new URL(location, trimmedUrl).href
+ if (
+ !isValidUrl(redirectUrl) ||
+ isPrivateHost(new URL(redirectUrl).hostname)
+ ) {
+ return Response.json(
+ { error: "Invalid or private redirect URL" },
+ { status: 400 },
+ )
+ }
+ // For simplicity, we only follow one level of redirect manually to re-validate IP
+ const secondResponse = await fetch(redirectUrl, {
+ signal: controller.signal,
+ redirect: "error", // No more redirects allowed
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
+ },
+ })
+ if (!secondResponse.ok) {
+ return Response.json(
+ { error: "Failed to fetch redirect URL" },
+ { status: secondResponse.status },
+ )
+ }
+ const contentType = secondResponse.headers.get("content-type")
+ if (contentType && !contentType.includes("text/html")) {
+ return Response.json({ title: "", description: "" })
+ }
+ const html = await secondResponse.text()
+ return processHtml(html, redirectUrl)
+ }
+ }
- if (!response.ok) {
- return Response.json(
- { error: "Failed to fetch URL" },
- { status: response.status },
- )
+ if (!response.ok) {
+ return Response.json(
+ { error: "Failed to fetch URL" },
+ { status: response.status },
+ )
+ }
+
+ const contentType = response.headers.get("content-type")
+ if (contentType && !contentType.includes("text/html")) {
+ return Response.json({ title: "", description: "" })
+ }
+
+ const html = await response.text()
+ return processHtml(html, trimmedUrl)
+ } finally {
+ clearTimeout(timeoutId)
}
-
- const html = await response.text()
-
- const titlePatterns = [
- /([^<]+)<\/title>/i,
- ]
-
- const descriptionPatterns = [
- /([^<]+)<\/title>/i,
+ ]
+
+ const descriptionPatterns = [
+ /