aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-04-18 08:55:15 +0000
committerFuwn <[email protected]>2026-04-18 08:55:15 +0000
commit92bf3d609699ebd810cfb770077c0f7714ee3119 (patch)
tree5c3f66c5d88f1242a7c9987964535dff2d8501c3
parentfix(api): encode subsplease timezone to prevent query-param injection (diff)
downloaddue.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.ts20
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")),