diff options
| author | Fuwn <[email protected]> | 2026-02-07 05:35:28 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 05:35:28 -0800 |
| commit | c4b2813cc07a72ad7186347a2e003c01cf0d4fb0 (patch) | |
| tree | ef9e2991084139e649dc7f6d3ada0795789a55f0 | |
| parent | fix: dynamically calculate detail panel equal split from current layout (diff) | |
| download | asa.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.ts | 6 | ||||
| -rw-r--r-- | apps/web/app/api/billing/create-checkout-session/route.ts | 10 | ||||
| -rw-r--r-- | apps/web/app/api/share/[token]/route.ts | 5 | ||||
| -rw-r--r-- | apps/web/app/api/share/route.ts | 26 | ||||
| -rw-r--r-- | apps/web/next.config.ts | 2 |
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'", |