diff options
| author | Fuwn <[email protected]> | 2026-04-18 08:55:15 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-04-18 08:55:15 +0000 |
| commit | 92bf3d609699ebd810cfb770077c0f7714ee3119 (patch) | |
| tree | 5c3f66c5d88f1242a7c9987964535dff2d8501c3 | |
| parent | fix(api): encode subsplease timezone to prevent query-param injection (diff) | |
| download | due.moe-92bf3d609699ebd810cfb770077c0f7714ee3119.tar.xz due.moe-92bf3d609699ebd810cfb770077c0f7714ee3119.zip | |
fix(api): gate badge click-count on Origin and fix 401 response reuse
The PUT ?incrementClickCount path ran before any auth guard, letting
unauthenticated callers spam-increment arbitrary badges. Require the
request Origin to match appOrigin() so legitimate in-browser clicks
(authenticated or not) still count while direct scripted calls are
rejected.
Also convert the shared `unauthorised` Response singleton into a
factory. The singleton's body was consumed on first use, so subsequent
401 paths returned a `Response body is locked` error instead of the
intended "Unauthorised" body.
| -rw-r--r-- | src/routes/api/badges/+server.ts | 20 |
1 files changed, 11 insertions, 9 deletions
diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts index 476fb264..a4d4ba93 100644 --- a/src/routes/api/badges/+server.ts +++ b/src/routes/api/badges/+server.ts @@ -15,10 +15,10 @@ import { incrementClickCount, } from "$lib/Database/SB/User/badges"; import { Schema } from "effect"; -import { appOriginHeaders } from "$lib/Utility/appOrigin"; +import { appOrigin, appOriginHeaders } from "$lib/Utility/appOrigin"; import privilegedUser from "$lib/Utility/privilegedUser"; -const unauthorised = new Response("Unauthorised", { status: 401 }); +const unauthorised = () => new Response("Unauthorised", { status: 401 }); const importedBadgeSchema = Schema.Record(Schema.String, Schema.Unknown); const badges = async (id: number) => @@ -33,15 +33,15 @@ export const GET = async ({ url }) => { export const DELETE = async ({ url, cookies }) => { const userCookie = cookies.get("user"); - if (!userCookie) return unauthorised; + if (!userCookie) return unauthorised(); const user = decodeAuthCookieOrNull(userCookie); - if (!user) return unauthorised; + if (!user) return unauthorised(); const identity = await safeUserIdentity(user); - if (!identity) return unauthorised; + if (!identity) return unauthorised(); if ((url.searchParams.get("prune") || 0) === "true") { await removeAllUserBadges(identity.id); @@ -54,6 +54,8 @@ export const DELETE = async ({ url, cookies }) => { export const PUT = async ({ cookies, url, request }) => { if (url.searchParams.get("incrementClickCount") || undefined) { + if (request.headers.get("origin") !== appOrigin()) return unauthorised(); + await incrementClickCount( Number(url.searchParams.get("incrementClickCount")), ); @@ -63,15 +65,15 @@ export const PUT = async ({ cookies, url, request }) => { const userCookie = cookies.get("user"); - if (!userCookie) return unauthorised; + if (!userCookie) return unauthorised(); const user = decodeAuthCookieOrNull(userCookie); - if (!user) return unauthorised; + if (!user) return unauthorised(); const identity = await safeUserIdentity(user); - if (!identity) return unauthorised; + if (!identity) return unauthorised(); const authorised = privilegedUser(identity.id); if (url.searchParams.get("shadowHide")) @@ -135,7 +137,7 @@ export const PUT = async ({ cookies, url, request }) => { } if (url.searchParams.get("shadowHideBadge") || undefined) { - if (!authorised) return unauthorised; + if (!authorised) return unauthorised(); await setShadowHiddenBadge( Number(url.searchParams.get("shadowHideBadge")), |