summaryrefslogtreecommitdiff
path: root/apps/web/app/api/webhook-config
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-08 09:21:50 -0800
committerFuwn <[email protected]>2026-02-08 09:21:50 -0800
commit396c3f450e0f17b77478a0525029aa2534e764a9 (patch)
treeac6d5f30c4cac6a7e9d07045d15b8388cd19791d /apps/web/app/api/webhook-config
parentsecurity: harden database functions and policies (diff)
downloadasa.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.ts50
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
}