Skip to content

Commit e0e7666

Browse files
committed
refactor(api): implement domain allow-list for URL validation in og and proxy handlers
1 parent c10f089 commit e0e7666

File tree

2 files changed

+32
-54
lines changed

2 files changed

+32
-54
lines changed

src/pages/api/v1/og.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import type { NextApiRequest, NextApiResponse } from "next";
22

3-
function isValidExternalUrl(url: string): boolean {
3+
// Allow-list of trusted domains for dApp OpenGraph fetching
4+
const ALLOWED_DOMAINS = [
5+
'fluidtokens.com',
6+
'aquarium-qa.fluidtokens.com',
7+
'minswap-multisig-dev.fluidtokens.com',
8+
// Add more trusted domains as needed
9+
];
10+
11+
function isAllowedDomain(url: string): boolean {
412
try {
513
const parsed = new URL(url);
614

@@ -9,30 +17,11 @@ function isValidExternalUrl(url: string): boolean {
917
return false;
1018
}
1119

12-
// Block private/internal IP ranges
13-
const hostname = parsed.hostname;
14-
15-
// Block localhost and loopback
16-
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
17-
return false;
18-
}
19-
20-
// Block private IP ranges (RFC 1918)
21-
const privateRanges = [
22-
/^10\./, // 10.0.0.0/8
23-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
24-
/^192\.168\./, // 192.168.0.0/16
25-
/^169\.254\./, // Link-local
26-
/^::1$/, // IPv6 loopback
27-
/^fc00:/, // IPv6 private
28-
/^fe80:/, // IPv6 link-local
29-
];
30-
31-
if (privateRanges.some(range => range.test(hostname))) {
32-
return false;
33-
}
34-
35-
return true;
20+
// Check if hostname is in allow-list
21+
const hostname = parsed.hostname.toLowerCase();
22+
return ALLOWED_DOMAINS.some(domain =>
23+
hostname === domain || hostname.endsWith('.' + domain)
24+
);
3625
} catch {
3726
return false;
3827
}
@@ -85,8 +74,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
8574
return res.status(400).json({ error: "Missing url parameter" });
8675
}
8776

88-
if (!isValidExternalUrl(url)) {
89-
return res.status(400).json({ error: "Invalid or unsafe URL" });
77+
if (!isAllowedDomain(url)) {
78+
return res.status(400).json({ error: "Domain not allowed" });
9079
}
9180

9281
try {

src/pages/api/v1/proxy.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import type { NextApiRequest, NextApiResponse } from "next";
22

3-
function isValidExternalUrl(url: string): boolean {
3+
// Allow-list of trusted domains for image proxying
4+
const ALLOWED_DOMAINS = [
5+
'fluidtokens.com',
6+
'aquarium-qa.fluidtokens.com',
7+
'minswap-multisig-dev.fluidtokens.com',
8+
// Add more trusted domains as needed
9+
];
10+
11+
function isAllowedDomain(url: string): boolean {
412
try {
513
const parsed = new URL(url);
614

@@ -9,30 +17,11 @@ function isValidExternalUrl(url: string): boolean {
917
return false;
1018
}
1119

12-
// Block private/internal IP ranges
13-
const hostname = parsed.hostname;
14-
15-
// Block localhost and loopback
16-
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
17-
return false;
18-
}
19-
20-
// Block private IP ranges (RFC 1918)
21-
const privateRanges = [
22-
/^10\./, // 10.0.0.0/8
23-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
24-
/^192\.168\./, // 192.168.0.0/16
25-
/^169\.254\./, // Link-local
26-
/^::1$/, // IPv6 loopback
27-
/^fc00:/, // IPv6 private
28-
/^fe80:/, // IPv6 link-local
29-
];
30-
31-
if (privateRanges.some(range => range.test(hostname))) {
32-
return false;
33-
}
34-
35-
return true;
20+
// Check if hostname is in allow-list
21+
const hostname = parsed.hostname.toLowerCase();
22+
return ALLOWED_DOMAINS.some(domain =>
23+
hostname === domain || hostname.endsWith('.' + domain)
24+
);
3625
} catch {
3726
return false;
3827
}
@@ -44,8 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
4433
return res.status(400).json({ error: "Missing src parameter" });
4534
}
4635

47-
if (!isValidExternalUrl(src)) {
48-
return res.status(400).json({ error: "Invalid or unsafe URL" });
36+
if (!isAllowedDomain(src)) {
37+
return res.status(400).json({ error: "Domain not allowed" });
4938
}
5039
try {
5140
const controller = new AbortController();

0 commit comments

Comments
 (0)