Skip to content

fix: proxy Discord webhook calls server-side to avoid CORS errors#239

Open
c14dd49h wants to merge 3 commits intoprojectdiscovery:mainfrom
c14dd49h:fix/discord-webhook-cors
Open

fix: proxy Discord webhook calls server-side to avoid CORS errors#239
c14dd49h wants to merge 3 commits intoprojectdiscovery:mainfrom
c14dd49h:fix/discord-webhook-cors

Conversation

@c14dd49h
Copy link
Copy Markdown

@c14dd49h c14dd49h commented Apr 3, 2026

Summary

Discord's REST API does not return Access-Control-Allow-Origin headers, 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:

Cross-Origin Request Blocked: The Same Origin policy disallows reading the remote resource
at https://discordapp.com/api/webhooks/...

Fix

  • Added a lightweight Next.js App Router API route (/api/discord-proxy) that relays Discord webhook calls server-side, where CORS restrictions do not apply
  • Updated notifyDiscord() in src/lib/notify/index.ts to call the local proxy instead of discordapp.com directly

The Telegram and Slack notification paths are unaffected.

Changes

  • New file: src/app/api/discord-proxy/route.ts — receives { webhook, embeds } from the client, extracts the webhook pathname, and forwards to discord.com
  • Modified: src/lib/notify/index.tsnotifyDiscord() now posts to /api/discord-proxy instead of https://discordapp.com

Test plan

  • Configure a Discord webhook URL in the interactsh-web settings panel
  • Trigger an OOB interaction
  • Verify the Discord notification arrives without CORS errors in the browser console
  • Verify npm run build succeeds and the /api/discord-proxy route appears in the build output

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>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 3, 2026

Someone is attempting to deploy a commit to the ProjectDiscovery Team on Vercel.

A member of the Team first needs to authorize it.

@neo-by-projectdiscovery-dev
Copy link
Copy Markdown

neo-by-projectdiscovery-dev bot commented Apr 3, 2026

Neo - PR Security Review

Critical: 2 · High: 2

Current PR state: 2 critical, 2 high active findings.

Highlights

  • Critical: SSRF: Unauthenticated open proxy to arbitrary Discord.com endpoints in src/app/api/discord-proxy/route.ts:24
  • Critical: SSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints in src/app/api/discord-proxy/route.ts:30
  • High: Missing authentication on Discord webhook proxy endpoint in src/app/api/discord-proxy/route.ts:12
Critical (2)
  • SSRF: Unauthenticated open proxy to arbitrary Discord.com endpointssrc/app/api/discord-proxy/route.ts:24
    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.
  • SSRF: Unrestricted pathname allows access to arbitrary Discord API endpointssrc/app/api/discord-proxy/route.ts:30
    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.
High (2)
  • Missing authentication on Discord webhook proxy endpointsrc/app/api/discord-proxy/route.ts:12
    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.
  • Missing authentication allows public proxy abusesrc/app/api/discord-proxy/route.ts:30
    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.
Security Impact

SSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints (src/app/api/discord-proxy/route.ts:30):
Attacker can access arbitrary Discord API endpoints through the server proxy, bypassing rate limits, hiding their origin IP, and enumerating Discord API routes without authentication. This converts a webhook proxy into a general-purpose Discord API proxy.

Missing authentication allows public proxy abuse (src/app/api/discord-proxy/route.ts:30):
External attackers can abuse the public server to send Discord API requests, bypassing Discord's rate limits and IP-based restrictions. The server's IP and resources can be used for Discord API enumeration, spam, or abuse without any authentication barrier.

Attack Examples

SSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints (src/app/api/discord-proxy/route.ts:30):

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.

Missing authentication allows public proxy abuse (src/app/api/discord-proxy/route.ts:30):

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.
Suggested Fixes

SSRF: Unrestricted pathname allows access to arbitrary Discord API endpoints (src/app/api/discord-proxy/route.ts:30):

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 }
  );
}

Missing authentication allows public proxy abuse (src/app/api/discord-proxy/route.ts:30):

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
🤖 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 }); }

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.
Hardening Notes
  • Consider adding rate limiting to the /api/discord-proxy endpoint to prevent abuse even after authentication is added
  • Add logging for proxy requests to monitor for suspicious patterns or abuse attempts

Comment @pdneo help for available commands. · Open in Neo

}

const pathname = new URL(webhook).pathname;
const res = await fetch(`https://discord.com${pathname}`, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

1 participant