diff options
| author | Fuwn <[email protected]> | 2026-02-10 00:06:15 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-10 00:06:15 -0800 |
| commit | 4dbc34c0261bb21d0109c31014b8b46abf7f20fd (patch) | |
| tree | 6672ce9e30d45f78ab2b43f7270d3f81d78d9496 /apps/web/app/api/webhook-config | |
| parent | fix: resolve Supabase security and performance advisories (diff) | |
| download | asa.news-4dbc34c0261bb21d0109c31014b8b46abf7f20fd.tar.xz asa.news-4dbc34c0261bb21d0109c31014b8b46abf7f20fd.zip | |
fix: P2 security hardening and tier limit parity
Webhook routes switched from admin client to server client (RLS).
Added DNS-resolution SSRF protection for webhook URLs with private IP
blocking. Added tier limit parity check script.
Diffstat (limited to 'apps/web/app/api/webhook-config')
| -rw-r--r-- | apps/web/app/api/webhook-config/route.ts | 57 | ||||
| -rw-r--r-- | apps/web/app/api/webhook-config/test/route.ts | 13 |
2 files changed, 19 insertions, 51 deletions
diff --git a/apps/web/app/api/webhook-config/route.ts b/apps/web/app/api/webhook-config/route.ts index aa63d0d..df2816f 100644 --- a/apps/web/app/api/webhook-config/route.ts +++ b/apps/web/app/api/webhook-config/route.ts @@ -1,8 +1,8 @@ import { NextResponse } from "next/server" import { createSupabaseServerClient } from "@/lib/supabase/server" -import { createSupabaseAdminClient } from "@/lib/supabase/admin" import { TIER_LIMITS, type SubscriptionTier } from "@asa-news/shared" import { rateLimit } from "@/lib/rate-limit" +import { validateWebhookUrl } from "@/lib/validate-webhook-url" import { checkBotId } from "botid/server" export async function GET() { @@ -15,8 +15,7 @@ export async function GET() { return NextResponse.json({ error: "not authenticated" }, { status: 401 }) } - const adminClient = createSupabaseAdminClient() - const { data: profile, error } = await adminClient + const { data: profile, error } = await supabaseClient .from("user_profiles") .select( "tier, webhook_url, webhook_secret, webhook_enabled, webhook_consecutive_failures" @@ -64,11 +63,9 @@ export async function PUT(request: Request) { return NextResponse.json({ error: "too many requests" }, { status: 429 }) } - const adminClient = createSupabaseAdminClient() - - const { data: profile } = await adminClient + const { data: profile } = await supabaseClient .from("user_profiles") - .select("tier") + .select("tier, webhook_url") .eq("id", user.id) .single() @@ -89,40 +86,10 @@ export async function PUT(request: Request) { if (typeof body.webhookUrl === "string") { const trimmedUrl = body.webhookUrl.trim() if (trimmedUrl) { - let parsedUrl: URL - try { - parsedUrl = new URL(trimmedUrl) - } catch { - return NextResponse.json( - { error: "invalid url format" }, - { status: 400 } - ) - } - if (parsedUrl.protocol !== "https:") { + const validationResult = await validateWebhookUrl(trimmedUrl) + if (!validationResult.valid) { 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" }, + { error: validationResult.error }, { status: 400 } ) } @@ -136,16 +103,10 @@ export async function PUT(request: Request) { if (typeof body.webhookEnabled === "boolean") { if (body.webhookEnabled) { - const { data: currentProfile } = await adminClient - .from("user_profiles") - .select("webhook_url") - .eq("id", user.id) - .single() - const effectiveUrl = typeof body.webhookUrl === "string" ? body.webhookUrl.trim() - : currentProfile?.webhook_url + : profile.webhook_url if (!effectiveUrl) { return NextResponse.json( @@ -164,7 +125,7 @@ export async function PUT(request: Request) { return NextResponse.json({ error: "no updates provided" }, { status: 400 }) } - const { error } = await adminClient + const { error } = await supabaseClient .from("user_profiles") .update(updates) .eq("id", user.id) diff --git a/apps/web/app/api/webhook-config/test/route.ts b/apps/web/app/api/webhook-config/test/route.ts index ae17c5b..81c3942 100644 --- a/apps/web/app/api/webhook-config/test/route.ts +++ b/apps/web/app/api/webhook-config/test/route.ts @@ -1,9 +1,9 @@ import { NextResponse } from "next/server" import { createHmac } from "crypto" import { createSupabaseServerClient } from "@/lib/supabase/server" -import { createSupabaseAdminClient } from "@/lib/supabase/admin" import { TIER_LIMITS, type SubscriptionTier } from "@asa-news/shared" import { rateLimit } from "@/lib/rate-limit" +import { validateWebhookUrl } from "@/lib/validate-webhook-url" import { checkBotId } from "botid/server" export async function POST() { @@ -26,8 +26,7 @@ export async function POST() { return NextResponse.json({ error: "too many requests" }, { status: 429 }) } - const adminClient = createSupabaseAdminClient() - const { data: profile } = await adminClient + const { data: profile } = await supabaseClient .from("user_profiles") .select( "tier, webhook_url, webhook_secret, webhook_enabled" @@ -52,6 +51,14 @@ export async function POST() { ) } + const validationResult = await validateWebhookUrl(profile.webhook_url) + if (!validationResult.valid) { + return NextResponse.json( + { error: validationResult.error }, + { status: 400 } + ) + } + const testPayload = { event: "test", timestamp: new Date().toISOString(), |