From 76d710493e2496490f9e2f9894cf581757f4d92e Mon Sep 17 00:00:00 2001 From: Fuwn Date: Tue, 2 Jun 2026 12:59:04 +0000 Subject: fix(security): replace RSS feed URL tokens with encrypted token (M5) --- src/routes/feeds/activity-notifications/+server.ts | 64 ++++++++++++++-------- 1 file changed, 41 insertions(+), 23 deletions(-) (limited to 'src/routes/feeds') diff --git a/src/routes/feeds/activity-notifications/+server.ts b/src/routes/feeds/activity-notifications/+server.ts index 64ba3fdc..145e236e 100644 --- a/src/routes/feeds/activity-notifications/+server.ts +++ b/src/routes/feeds/activity-notifications/+server.ts @@ -2,16 +2,15 @@ import { type Notification, notifications, } from "$lib/Data/AniList/notifications"; +import { refreshAniListToken } from "$lib/Utility/anilistOauth"; import { siteUrl } from "$lib/Utility/appOrigin"; -import root from "$lib/Utility/root"; +import { decryptFeedToken } from "$lib/Utility/feedToken"; const htmlEncode = (input: string) => { return input.replace(/[\u00A0-\u9999<>&]/g, (i) => `&#${i.charCodeAt(0)};`); }; -const render = ( - posts: Notification[] = [], -) => ` +const channel = (items: string) => ` ${new Date().toUTCString()} en-US ${siteUrl("/favicon-196x196.png")} - ${posts + ${items} + + +`; + +const render = (posts: Notification[] = []) => + channel( + posts .filter((notification: Notification) => notification.type !== undefined) .map((notification: Notification) => { let title = `${notification.user.name}${notification.context}`; @@ -66,28 +72,40 @@ xmlns:media="http://search.yahoo.com/mrss/"> ${new Date(notification.createdAt * 1000).toUTCString()} `; }) - .join("")} - - -`; - -export const GET = async ({ url }) => { - let token = url.searchParams.get("token"); - const refresh = url.searchParams.get("refresh"); - let notification = await notifications(token || ""); + .join(""), + ); - if (notification === null) { - token = ( - await (await fetch(root(`/api/oauth/refresh?token=${refresh}`))).json() - ).access_token; +// Old feed URLs carried the tokens in clear (?token=&refresh=); they no longer +// resolve, so degrade to a single item nudging the user to re-copy the link +// instead of silently going empty. +const staleNotice = () => + channel(` +due-moe-feed-url-outdated +${htmlEncode("Your due.moe RSS feed URL is outdated — re-copy it from Settings")} +${siteUrl("/settings#feeds")} +${htmlEncode("This feed link no longer works. Open due.moe → Settings → RSS Feeds and copy the new URL into your reader.")} +${new Date().toUTCString()} +`); - notification = await notifications(token as string); - } - - return new Response(token ? render(notification || []) : render(), { +const feed = (body: string) => + new Response(body, { headers: { - "Cache-Control": `max-age=0`, + "Cache-Control": "max-age=0", "Content-Type": "application/xml", }, }); + +export const GET = async ({ url }) => { + const sealed = url.searchParams.get("feed"); + const refreshToken = sealed ? await decryptFeedToken(sealed) : null; + + if (!refreshToken) return feed(staleNotice()); + + const accessToken = await refreshAniListToken(refreshToken); + + if (!accessToken) return feed(render()); + + const notification = await notifications(accessToken); + + return feed(render(notification || [])); }; -- cgit v1.2.3