summaryrefslogtreecommitdiff
path: root/apps/web/app/api/webhook-config
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-10 00:06:15 -0800
committerFuwn <[email protected]>2026-02-10 00:06:15 -0800
commit4dbc34c0261bb21d0109c31014b8b46abf7f20fd (patch)
tree6672ce9e30d45f78ab2b43f7270d3f81d78d9496 /apps/web/app/api/webhook-config
parentfix: resolve Supabase security and performance advisories (diff)
downloadasa.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.ts57
-rw-r--r--apps/web/app/api/webhook-config/test/route.ts13
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(),