diff options
| author | Fuwn <[email protected]> | 2026-02-08 09:21:50 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-08 09:21:50 -0800 |
| commit | 396c3f450e0f17b77478a0525029aa2534e764a9 (patch) | |
| tree | ac6d5f30c4cac6a7e9d07045d15b8388cd19791d /apps/web/app/api/webhook-config | |
| parent | security: harden database functions and policies (diff) | |
| download | asa.news-396c3f450e0f17b77478a0525029aa2534e764a9.tar.xz asa.news-396c3f450e0f17b77478a0525029aa2534e764a9.zip | |
security: harden API routes
- Add rate limiting to /api/share (30/min), /api/export (5/hr),
/api/account/data (3/day)
- Add client-side 30s throttle to forgot-password form
- Remove immediate tier upgrade on plan change; let invoice.paid
webhook handle tier promotion to prevent free upgrades on payment
failure
- Add SSRF validation to webhook URLs: block localhost, private IPs,
link-local, and metadata endpoints
- Log Stripe webhook signature verification errors instead of
swallowing silently
- Mask webhook secret in GET response (show first/last 4 chars only)
- Add error logging to API key last_used_at update
- Remove internal error message leaking from checkout session route
Diffstat (limited to 'apps/web/app/api/webhook-config')
| -rw-r--r-- | apps/web/app/api/webhook-config/route.ts | 50 |
1 files changed, 44 insertions, 6 deletions
diff --git a/apps/web/app/api/webhook-config/route.ts b/apps/web/app/api/webhook-config/route.ts index dcfd052..eefa9f2 100644 --- a/apps/web/app/api/webhook-config/route.ts +++ b/apps/web/app/api/webhook-config/route.ts @@ -31,9 +31,14 @@ export async function GET() { ) } + const maskedSecret = profile.webhook_secret + ? profile.webhook_secret.slice(0, 4) + "••••" + profile.webhook_secret.slice(-4) + : null + return NextResponse.json({ webhookUrl: profile.webhook_url, - webhookSecret: profile.webhook_secret, + webhookSecret: maskedSecret, + webhookSecretConfigured: !!profile.webhook_secret, webhookEnabled: profile.webhook_enabled, consecutiveFailures: profile.webhook_consecutive_failures, }) @@ -83,11 +88,44 @@ export async function PUT(request: Request) { if (typeof body.webhookUrl === "string") { const trimmedUrl = body.webhookUrl.trim() - if (trimmedUrl && !trimmedUrl.startsWith("https://")) { - return NextResponse.json( - { error: "webhook url must use https" }, - { status: 400 } - ) + if (trimmedUrl) { + let parsedUrl: URL + try { + parsedUrl = new URL(trimmedUrl) + } catch { + return NextResponse.json( + { error: "invalid url format" }, + { status: 400 } + ) + } + if (parsedUrl.protocol !== "https:") { + return NextResponse.json( + { error: "webhook url must use https" }, + { status: 400 } + ) + } + const hostname = parsedUrl.hostname + if ( + hostname === "localhost" || + hostname.startsWith("127.") || + hostname.startsWith("10.") || + hostname.startsWith("192.168.") || + hostname.startsWith("172.16.") || + hostname.startsWith("172.17.") || + hostname.startsWith("172.18.") || + hostname.startsWith("172.19.") || + hostname.startsWith("172.2") || + hostname.startsWith("172.30.") || + hostname.startsWith("172.31.") || + hostname === "169.254.169.254" || + hostname === "[::1]" || + hostname === "0.0.0.0" + ) { + return NextResponse.json( + { error: "webhook url must not point to internal addresses" }, + { status: 400 } + ) + } } updates.webhook_url = trimmedUrl || null } |