summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 05:35:28 -0800
committerFuwn <[email protected]>2026-02-07 05:35:28 -0800
commitc4b2813cc07a72ad7186347a2e003c01cf0d4fb0 (patch)
treeef9e2991084139e649dc7f6d3ada0795789a55f0
parentfix: dynamically calculate detail panel equal split from current layout (diff)
downloadasa.news-c4b2813cc07a72ad7186347a2e003c01cf0d4fb0.tar.xz
asa.news-c4b2813cc07a72ad7186347a2e003c01cf0d4fb0.zip
security: remove unsafe-eval CSP, fix host header injection, harden API routes
- Remove unsafe-eval from script-src CSP (not needed in production) - Replace Host/Origin header fallback with NEXT_PUBLIC_APP_URL in share and checkout routes to prevent host header injection - Add .catch() to request.json() in share POST and PATCH routes - Add rate limiting (3/min) to account deletion endpoint
-rw-r--r--apps/web/app/api/account/route.ts6
-rw-r--r--apps/web/app/api/billing/create-checkout-session/route.ts10
-rw-r--r--apps/web/app/api/share/[token]/route.ts5
-rw-r--r--apps/web/app/api/share/route.ts26
-rw-r--r--apps/web/next.config.ts2
5 files changed, 33 insertions, 16 deletions
diff --git a/apps/web/app/api/account/route.ts b/apps/web/app/api/account/route.ts
index 35408d7..1212541 100644
--- a/apps/web/app/api/account/route.ts
+++ b/apps/web/app/api/account/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server"
import { createSupabaseServerClient } from "@/lib/supabase/server"
import { createSupabaseAdminClient } from "@/lib/supabase/admin"
+import { rateLimit } from "@/lib/rate-limit"
export async function DELETE() {
const supabaseClient = await createSupabaseServerClient()
@@ -12,6 +13,11 @@ export async function DELETE() {
return NextResponse.json({ error: "not authenticated" }, { status: 401 })
}
+ const rateLimitResult = rateLimit(`account-delete:${user.id}`, 3, 60_000)
+ if (!rateLimitResult.success) {
+ return NextResponse.json({ error: "too many requests" }, { status: 429 })
+ }
+
const adminClient = createSupabaseAdminClient()
const { error } = await adminClient.auth.admin.deleteUser(user.id)
diff --git a/apps/web/app/api/billing/create-checkout-session/route.ts b/apps/web/app/api/billing/create-checkout-session/route.ts
index d165cbc..4eaa08c 100644
--- a/apps/web/app/api/billing/create-checkout-session/route.ts
+++ b/apps/web/app/api/billing/create-checkout-session/route.ts
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server"
-import { headers } from "next/headers"
import { createSupabaseServerClient } from "@/lib/supabase/server"
import { createSupabaseAdminClient } from "@/lib/supabase/admin"
import { getStripe } from "@/lib/stripe"
@@ -129,8 +128,13 @@ export async function POST(request: Request) {
}
}
- const headersList = await headers()
- const origin = headersList.get("origin") || "http://localhost:3000"
+ const origin = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, "")
+ if (!origin) {
+ return NextResponse.json(
+ { error: "application URL is not configured" },
+ { status: 500 }
+ )
+ }
const checkoutSession = await getStripe().checkout.sessions.create({
customer: stripeCustomerIdentifier,
diff --git a/apps/web/app/api/share/[token]/route.ts b/apps/web/app/api/share/[token]/route.ts
index d1d57b5..20de1ae 100644
--- a/apps/web/app/api/share/[token]/route.ts
+++ b/apps/web/app/api/share/[token]/route.ts
@@ -48,7 +48,10 @@ export async function PATCH(
}
const { token } = await params
- const body = await request.json()
+ const body = await request.json().catch(() => null)
+ if (!body || typeof body !== "object") {
+ return NextResponse.json({ error: "invalid request body" }, { status: 400 })
+ }
const rawNote = body.note
let note: string | null = null
diff --git a/apps/web/app/api/share/route.ts b/apps/web/app/api/share/route.ts
index f330bd0..e1252bf 100644
--- a/apps/web/app/api/share/route.ts
+++ b/apps/web/app/api/share/route.ts
@@ -4,15 +4,10 @@ import { createSupabaseServerClient } from "@/lib/supabase/server"
const MAX_NOTE_LENGTH = 1000
-function buildOrigin(request: Request): string {
- if (process.env.NEXT_PUBLIC_APP_URL) {
- return process.env.NEXT_PUBLIC_APP_URL.replace(/\/$/, "")
- }
-
- return (
- request.headers.get("origin") ??
- `https://${request.headers.get("host")}`
- )
+function getAppOrigin(): string | null {
+ const url = process.env.NEXT_PUBLIC_APP_URL
+ if (!url) return null
+ return url.replace(/\/$/, "")
}
export async function POST(request: Request) {
@@ -37,7 +32,10 @@ export async function POST(request: Request) {
Date.now() + expiryDays * 24 * 60 * 60 * 1000
).toISOString()
- const body = await request.json()
+ const body = await request.json().catch(() => null)
+ if (!body || typeof body !== "object") {
+ return NextResponse.json({ error: "invalid request body" }, { status: 400 })
+ }
const entryIdentifier = body.entryIdentifier as string
const rawNote = body.note
@@ -92,7 +90,13 @@ export async function POST(request: Request) {
)
}
- const origin = buildOrigin(request)
+ const origin = getAppOrigin()
+ if (!origin) {
+ return NextResponse.json(
+ { error: "application URL is not configured" },
+ { status: 500 }
+ )
+ }
const { data: existingShare } = await supabaseClient
.from("shared_entries")
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index f580efd..4d35ae1 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -23,7 +23,7 @@ const securityHeaders = [
key: "Content-Security-Policy",
value: [
"default-src 'self'",
- "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com",
+ "script-src 'self' 'unsafe-inline' https://va.vercel-scripts.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https: http:",
"font-src 'self'",