From 8a99dd5c4b74a4ea2ce715aed5e517022621f05c Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sat, 28 Mar 2026 06:02:54 +0000 Subject: fix(auth): ignore malformed user cookies --- src/graphql/user/resolvers.ts | 13 +++++++++---- src/hooks.server.ts | 9 +++++++-- src/lib/Data/AniList/identity.ts | 14 ++++++++++++++ src/lib/Effect/authCookie.test.ts | 18 ++++++++++++++++++ src/lib/Effect/authCookie.ts | 14 +++++++++++--- src/routes/api/badges/+server.ts | 22 ++++++++++++++++------ src/routes/api/configuration/+server.ts | 10 +++++++--- src/routes/api/notifications/subscribe/+server.ts | 11 +++++++---- .../api/notifications/unsubscribe/+server.ts | 11 +++++++---- src/routes/api/preferences/+server.ts | 10 +++++++--- src/routes/api/preferences/pin/+server.ts | 14 ++++++++++---- 11 files changed, 113 insertions(+), 33 deletions(-) diff --git a/src/graphql/user/resolvers.ts b/src/graphql/user/resolvers.ts index dfbaa927..360d622b 100644 --- a/src/graphql/user/resolvers.ts +++ b/src/graphql/user/resolvers.ts @@ -1,4 +1,7 @@ -import { userIdentity, type UserIdentity } from "$lib/Data/AniList/identity"; +import { + safeUserIdentity, + type UserIdentity, +} from "$lib/Data/AniList/identity"; import { addUserBadge, getUserBadges, @@ -24,7 +27,7 @@ import { type UserPreferences, } from "$lib/Database/SB/User/preferences"; import privilegedUser from "$lib/Utility/privilegedUser"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; type Context = RequestEvent>, string | null>; type UserResolvers = Pick< @@ -52,9 +55,11 @@ const auth = async (context: Context) => { if (!userCookie) return Error("Unauthorised"); - const user = decodeAuthCookieOrThrow(userCookie); + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return Error("Unauthorised"); - return await userIdentity(user); + return (await safeUserIdentity(user)) ?? Error("Unauthorised"); }; const authenticatedBadgesOperation = async ( diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 9f28b4e9..ca4fd8d3 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,5 @@ import root from "$lib/Utility/root"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; import type { Handle, RequestEvent } from "@sveltejs/kit"; const redirectWithParameters = ( @@ -22,7 +22,12 @@ export const handle: Handle = async ({ event, resolve }) => { const { cookies } = event; const user = cookies.get("user"); - if (user) event.locals.user = decodeAuthCookieOrThrow(user); + if (user) { + const decodedUser = decodeAuthCookieOrNull(user); + + if (decodedUser) event.locals.user = decodedUser; + else cookies.delete("user", { path: "/" }); + } switch (event.url.pathname) { case "/birthdays": diff --git a/src/lib/Data/AniList/identity.ts b/src/lib/Data/AniList/identity.ts index eacc2ae4..973e1184 100644 --- a/src/lib/Data/AniList/identity.ts +++ b/src/lib/Data/AniList/identity.ts @@ -34,3 +34,17 @@ export const userIdentity = async ( avatar: userIdResponse["data"]["Viewer"]["avatar"]["large"], }; }; + +export const safeUserIdentity = async ( + anilistAuthorisation: AniListAuthorisation, +): Promise => { + try { + const identity = await userIdentity(anilistAuthorisation); + + if (!identity.id || !identity.name || !identity.avatar) return null; + + return identity; + } catch { + return null; + } +}; diff --git a/src/lib/Effect/authCookie.test.ts b/src/lib/Effect/authCookie.test.ts index 2a27f0ce..bdcc4561 100644 --- a/src/lib/Effect/authCookie.test.ts +++ b/src/lib/Effect/authCookie.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { Result } from "effect"; import { decodeAuthCookieEither, + decodeAuthCookieOrNull, decodeAuthCookieOrThrow, } from "$lib/Effect/authCookie"; @@ -44,4 +45,21 @@ describe("decodeAuthCookie", () => { it("throws on invalid payload through decodeAuthCookieOrThrow", () => { expect(() => decodeAuthCookieOrThrow("{oops")).toThrowError(); }); + + it("returns null on invalid payload through decodeAuthCookieOrNull", () => { + expect(decodeAuthCookieOrNull("{oops")).toBeNull(); + }); + + it("returns null for schema-valid but empty auth fields", () => { + expect( + decodeAuthCookieOrNull( + JSON.stringify({ + token_type: "Bearer", + expires_in: 3600, + access_token: "", + refresh_token: "", + }), + ), + ).toBeNull(); + }); }); diff --git a/src/lib/Effect/authCookie.ts b/src/lib/Effect/authCookie.ts index e716f5e9..f407216e 100644 --- a/src/lib/Effect/authCookie.ts +++ b/src/lib/Effect/authCookie.ts @@ -2,10 +2,10 @@ import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; import { Effect, Result, Schema } from "effect"; const UserCookieSchema = Schema.Struct({ - token_type: Schema.String, + token_type: Schema.NonEmptyString, expires_in: Schema.Number, - access_token: Schema.String, - refresh_token: Schema.String, + access_token: Schema.NonEmptyString, + refresh_token: Schema.NonEmptyString, }); export const decodeAuthCookieEffect = (cookie: string) => @@ -43,3 +43,11 @@ export const decodeAuthCookieOrThrow = ( return Result.getOrThrow(decoded); }; + +export const decodeAuthCookieOrNull = ( + cookie: string, +): AniListAuthorisation | null => { + const decoded = decodeAuthCookieEither(cookie); + + return Result.isSuccess(decoded) ? decoded.success : null; +}; diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts index 8a86b468..476fb264 100644 --- a/src/routes/api/badges/+server.ts +++ b/src/routes/api/badges/+server.ts @@ -1,5 +1,5 @@ -import { userIdentity } from "$lib/Data/AniList/identity"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; import { removeAllUserBadges, @@ -35,8 +35,13 @@ export const DELETE = async ({ url, cookies }) => { if (!userCookie) return unauthorised; - const user = decodeAuthCookieOrThrow(userCookie); - const identity = await userIdentity(user); + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return unauthorised; + + const identity = await safeUserIdentity(user); + + if (!identity) return unauthorised; if ((url.searchParams.get("prune") || 0) === "true") { await removeAllUserBadges(identity.id); @@ -60,8 +65,13 @@ export const PUT = async ({ cookies, url, request }) => { if (!userCookie) return unauthorised; - const user = decodeAuthCookieOrThrow(userCookie); - const identity = await userIdentity(user); + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return unauthorised; + + const identity = await safeUserIdentity(user); + + if (!identity) return unauthorised; const authorised = privilegedUser(identity.id); if (url.searchParams.get("shadowHide")) diff --git a/src/routes/api/configuration/+server.ts b/src/routes/api/configuration/+server.ts index 786e8333..306e1285 100644 --- a/src/routes/api/configuration/+server.ts +++ b/src/routes/api/configuration/+server.ts @@ -1,11 +1,11 @@ import { Schema } from "effect"; -import { userIdentity } from "$lib/Data/AniList/identity"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; import { deleteUserConfiguration, getUserConfiguration, setUserConfiguration, } from "$lib/Database/SB/User/configuration"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; import { appOriginHeaders } from "$lib/Utility/appOrigin"; @@ -18,7 +18,11 @@ const authenticatedUserId = async (cookies: { if (!userCookie) return null; - return (await userIdentity(decodeAuthCookieOrThrow(userCookie))).id; + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return null; + + return (await safeUserIdentity(user))?.id ?? null; }; export const GET = async ({ cookies, url }) => { diff --git a/src/routes/api/notifications/subscribe/+server.ts b/src/routes/api/notifications/subscribe/+server.ts index 806785e4..203470e0 100644 --- a/src/routes/api/notifications/subscribe/+server.ts +++ b/src/routes/api/notifications/subscribe/+server.ts @@ -1,6 +1,6 @@ -import { userIdentity } from "$lib/Data/AniList/identity"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; import { setUserSubscription } from "$lib/Database/SB/User/notifications"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; import { Schema } from "effect"; @@ -12,8 +12,11 @@ export const POST = async ({ cookies, request, url }) => { if (!userCookie || !fingerprint) return unauthorised; - const user = decodeAuthCookieOrThrow(userCookie); - const userId = (await userIdentity(user)).id; + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return unauthorised; + + const userId = (await safeUserIdentity(user))?.id; if (!userId) return unauthorised; diff --git a/src/routes/api/notifications/unsubscribe/+server.ts b/src/routes/api/notifications/unsubscribe/+server.ts index 87f8b498..94bbd497 100644 --- a/src/routes/api/notifications/unsubscribe/+server.ts +++ b/src/routes/api/notifications/unsubscribe/+server.ts @@ -1,6 +1,6 @@ -import { userIdentity } from "$lib/Data/AniList/identity"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; import { deleteUserSubscription } from "$lib/Database/SB/User/notifications"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; const unauthorised = new Response("Unauthorised", { status: 401 }); @@ -10,8 +10,11 @@ export const POST = async ({ cookies, url }) => { if (!userCookie || !fingerprint) return unauthorised; - const user = decodeAuthCookieOrThrow(userCookie); - const userId = (await userIdentity(user)).id; + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return unauthorised; + + const userId = (await safeUserIdentity(user))?.id; if (!userId) return unauthorised; diff --git a/src/routes/api/preferences/+server.ts b/src/routes/api/preferences/+server.ts index 0537c9bc..47ce442b 100644 --- a/src/routes/api/preferences/+server.ts +++ b/src/routes/api/preferences/+server.ts @@ -1,5 +1,5 @@ import { Schema } from "effect"; -import { userIdentity } from "$lib/Data/AniList/identity"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; import { getUserPreferences, setBiography, @@ -9,7 +9,7 @@ import { toggleHideMissingBadges, togglePinnedBadgeWallCategory, } from "$lib/Database/SB/User/preferences"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; import { appOriginHeaders } from "$lib/Utility/appOrigin"; @@ -22,7 +22,11 @@ const authenticatedUserId = async (cookies: { if (!userCookie) return null; - return (await userIdentity(decodeAuthCookieOrThrow(userCookie))).id; + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return null; + + return (await safeUserIdentity(user))?.id ?? null; }; export const GET = async ({ url }) => { diff --git a/src/routes/api/preferences/pin/+server.ts b/src/routes/api/preferences/pin/+server.ts index 45a231fd..b69a8142 100644 --- a/src/routes/api/preferences/pin/+server.ts +++ b/src/routes/api/preferences/pin/+server.ts @@ -1,5 +1,5 @@ -import { userIdentity } from "$lib/Data/AniList/identity"; -import { decodeAuthCookieOrThrow } from "$lib/Effect/authCookie"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; import { toggleHololiveStreamPinning } from "$lib/Database/SB/User/preferences"; import { appOriginHeaders } from "$lib/Utility/appOrigin"; @@ -10,11 +10,17 @@ export const PUT = async ({ cookies, url }) => { if (!userCookie) return unauthorised; - const user = decodeAuthCookieOrThrow(userCookie); + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return unauthorised; + + const identity = await safeUserIdentity(user); + + if (!identity) return unauthorised; return Response.json( await toggleHololiveStreamPinning( - (await userIdentity(user)).id, + identity.id, url.searchParams.get("stream") || "", ), { -- cgit v1.2.3