fix: proxy Discord webhook calls server-side to avoid CORS errors#239
fix: proxy Discord webhook calls server-side to avoid CORS errors#239c14dd49h wants to merge 3 commits intoprojectdiscovery:mainfrom
Conversation
Discord's API does not return CORS headers, so browser-initiated fetch requests to discordapp.com are blocked by the Same-Origin policy. This makes the Discord notification feature in the web UI completely non-functional. This fix adds a lightweight Next.js API route (/api/discord-proxy) that relays Discord webhook calls server-side, where CORS does not apply. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Someone is attempting to deploy a commit to the ProjectDiscovery Team on Vercel. A member of the Team first needs to authorize it. |
Neo - PR Security ReviewCritical: 2 · High: 2 Current PR state: 2 critical, 2 high active findings. Highlights
Critical (2)
High (2)
Security ImpactSSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints ( Missing authentication allows public proxy abuse ( Attack ExamplesSSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints ( Missing authentication allows public proxy abuse ( Suggested FixesSSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints ( Missing authentication allows public proxy abuse ( 🤖 Prompt for AI AgentsHardening Notes
Comment |
| } | ||
|
|
||
| const pathname = new URL(webhook).pathname; | ||
| const res = await fetch(`https://discord.com${pathname}`, { |
There was a problem hiding this comment.
🔴 SSRF: Unauthenticated open proxy to arbitrary Discord.com endpoints (CWE-918) — The Discord webhook proxy extracts the pathname from a user-controlled webhook parameter and concatenates it with https://discord.com to construct the destination URL, without validating that the original webhook URL's hostname is actually a Discord domain or that the pathname matches the expected Discord webhook format (/api/webhooks/{id}/{token}). An attacker can supply an arbitrary URL with any pathname, and the server will forward POST requests to https://discord.com{pathname} with the attacker-controlled embeds payload.
Suggested Fix
Before extracting pathname, validate that the webhook URL hostname is exactly `discord.com` or `discordapp.com`, and that the pathname matches the Discord webhook format `/api/webhooks/{snowflake}/{token}` using a regex. Reject any webhook URL that doesn't meet these criteria. Also add authentication/authorization to prevent external abuse (see finding #missing-auth-discord-proxy-2).
Attack Example
POST /api/discord-proxy
{
"webhook": "https://evil.com/api/v10/users/@me",
"embeds": [{"description": "malicious payload"}]
}
The server extracts pathname `/api/v10/users/@me` and sends POST to `https://discord.com/api/v10/users/@me`. Alternatively, an attacker can spam victim webhooks: {"webhook": "https://attacker.com/api/webhooks/victim-id/victim-token", "embeds": [spam]} proxies spam from the server's IP.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/discord-proxy/route.ts` at lines 23-24, the code extracts
pathname from the user-controlled webhook parameter and concatenates it with
https://discord.com without validating the original hostname or pathname format.
Add validation after line 14: parse the webhook URL, check that `url.hostname`
is exactly 'discord.com' or 'discordapp.com', verify the pathname matches
`/api/webhooks/\d+/[\w-]+` regex pattern, and return 400 error if validation
fails. This prevents SSRF by ensuring only legitimate Discord webhook URLs are
proxied.
| * client, then forwards them to Discord from the server where CORS does | ||
| * not apply. | ||
| */ | ||
| export async function POST(req: NextRequest) { |
There was a problem hiding this comment.
🟠 Missing authentication on Discord webhook proxy endpoint (CWE-306) — The /api/discord-proxy endpoint is a public Next.js App Router API route with no authentication middleware, session validation, or authorization checks. Any external attacker can POST to this endpoint and trigger server-side Discord webhook requests. Combined with the SSRF vulnerability (finding #ssrf-discord-proxy-1), this creates an unauthenticated open proxy that external attackers can abuse at will.
Suggested Fix
Add authentication to the POST handler. Options: (1) verify the request originates from the same origin (check referer/origin headers and reject external requests), (2) require a session cookie or JWT token that proves the user is authenticated to the web application, (3) use Next.js middleware to protect `/api/discord-proxy` with session-based authentication, or (4) implement CSRF tokens. For a webhook proxy, origin validation is typically sufficient: reject requests where the Origin header is not from the same domain.
Attack Example
# External attacker script
curl -X POST https://target-server.com/api/discord-proxy \
-H "Content-Type: application/json" \
-d '{"webhook": "https://discord.com/api/webhooks/victim-id/victim-token", "embeds": [{"description": "spam"}]}'
No credentials required. Attacker can loop this to spam webhooks or exhaust server resources.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/discord-proxy/route.ts` at line 12, the POST handler has no
authentication. Add origin validation at the start of the handler (after line
13): extract the Origin or Referer header from req.headers, verify it matches
the application's own domain (not an external origin), and return
NextResponse.json({ error: 'Forbidden' }, { status: 403 }) if the origin is
missing or external. This prevents external attackers from abusing the proxy
while allowing legitimate same-origin requests from the web UI.
Discord returns 204 with no body on successful webhook delivery. NextResponse cannot construct a JSON response with status 204, causing a TypeError. Return an empty response for 204 instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| body: JSON.stringify({ embeds }), | ||
| }); | ||
|
|
||
| if (res.status === 204) { |
There was a problem hiding this comment.
🔴 SSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints (CWE-918) — The Discord proxy extracts the pathname from the user-provided webhook URL and forwards it to discord.com without validating that it's actually a webhook path. An attacker can submit any pathname (e.g., /api/users/@me, /api/guilds/{id}) and the server will proxy the request to that Discord API endpoint.
Suggested Fix
Validate that the pathname starts with `/api/webhooks/` before proxying:
const pathname = new URL(webhook).pathname;
if (!pathname.startsWith('/api/webhooks/')) {
return NextResponse.json(
{ error: 'Invalid webhook URL' },
{ status: 400 }
);
}
Attack Example
POST /api/discord-proxy
{
"webhook": "https://malicious.com/api/users/@me",
"embeds": []
}
Server sends POST to https://discord.com/api/users/@me instead of a webhook endpoint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/discord-proxy/route.ts` after line 23 where pathname is
extracted, add validation to ensure it starts with '/api/webhooks/' before line
24. Insert: if (!pathname.startsWith('/api/webhooks/')) { return
NextResponse.json({ error: 'Invalid webhook URL' }, { status: 400 }); }
| body: JSON.stringify({ embeds }), | ||
| }); | ||
|
|
||
| if (res.status === 204) { |
There was a problem hiding this comment.
🟠 Missing authentication allows public proxy abuse (CWE-306) — The /api/discord-proxy endpoint has no authentication or authorization mechanism. Any external attacker can POST to this endpoint and use the server as a Discord API proxy. Combined with the pathname SSRF, this allows unlimited unauthorized Discord API access.
Suggested Fix
Add authentication to restrict the API route to legitimate users. Options include:
1. Require session cookies or JWT tokens
2. Validate requests come from the same origin (check Referer/Origin headers)
3. Use Next.js middleware to protect all /api routes
4. Add rate limiting per IP address
Attack Example
curl -X POST https://interactsh-web.com/api/discord-proxy \
-H 'Content-Type: application/json' \
-d '{"webhook":"https://x.com/api/webhooks/123/token","embeds":[{"description":"spam"}]}'
No authentication required - any internet user can abuse this proxy.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/discord-proxy/route.ts` at the start of the POST handler (line
12), add origin validation to ensure requests come from the application itself.
Check that the Origin or Referer header matches the application's domain, or
implement session-based authentication to prevent external abuse of the proxy
endpoint.
Restrict the proxy to Discord webhook paths (/api/webhooks/<id>/<token>) only, preventing misuse as a proxy to arbitrary discord.com endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Discord's REST API does not return
Access-Control-Allow-Originheaders, which means all browser-initiated fetch requests to Discord webhook URLs are blocked by the Same-Origin policy. This makes the Discord notification feature in the web UI completely non-functional — every notification attempt results in a CORS error:Fix
/api/discord-proxy) that relays Discord webhook calls server-side, where CORS restrictions do not applynotifyDiscord()insrc/lib/notify/index.tsto call the local proxy instead ofdiscordapp.comdirectlyThe Telegram and Slack notification paths are unaffected.
Changes
src/app/api/discord-proxy/route.ts— receives{ webhook, embeds }from the client, extracts the webhook pathname, and forwards todiscord.comsrc/lib/notify/index.ts—notifyDiscord()now posts to/api/discord-proxyinstead ofhttps://discordapp.comTest plan
npm run buildsucceeds and the/api/discord-proxyroute appears in the build output