From 7c87edc28a8bce42933d27ac9753ec8adb7ba51a Mon Sep 17 00:00:00 2001 From: matty Date: Sat, 14 Feb 2026 16:30:21 +0000 Subject: [PATCH] patch XSS, injection vuln --- packages/pl-fe/src/actions/external-auth.ts | 8 +++++- .../pl-fe/src/components/preview-card.tsx | 8 +++++- .../pl-fe/src/components/scrollable-list.tsx | 8 +++++- packages/pl-fe/src/pages/utils/about.tsx | 3 ++- packages/pl-fe/src/reducers/auth.ts | 26 +++++++++++-------- packages/pl-fe/src/utils/redirect.ts | 7 ++++- 6 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/pl-fe/src/actions/external-auth.ts b/packages/pl-fe/src/actions/external-auth.ts index f800d388f..7859d34dc 100644 --- a/packages/pl-fe/src/actions/external-auth.ts +++ b/packages/pl-fe/src/actions/external-auth.ts @@ -73,7 +73,13 @@ const externalLogin = (host: string) => { const loginWithCode = (code: string) => (dispatch: AppDispatch) => { - const app = JSON.parse(localStorage.getItem('plfe:external:app')!); + let app; + try { + app = JSON.parse(localStorage.getItem('plfe:external:app')!); + } catch { + return; + } + if (!app) return; const { client_id, client_secret, redirect_uri } = app; const baseURL = localStorage.getItem('plfe:external:baseurl')!; const scope = localStorage.getItem('plfe:external:scopes')!; diff --git a/packages/pl-fe/src/components/preview-card.tsx b/packages/pl-fe/src/components/preview-card.tsx index 3e01c05af..09d114998 100644 --- a/packages/pl-fe/src/components/preview-card.tsx +++ b/packages/pl-fe/src/components/preview-card.tsx @@ -1,10 +1,12 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; +import DOMPurify from 'isomorphic-dompurify'; import { type MediaAttachment, type PreviewCard as CardEntity, mediaAttachmentSchema } from 'pl-api'; import React, { useState, useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import * as v from 'valibot'; + import Blurhash from '@/components/blurhash'; import HStack from '@/components/ui/hstack'; import Icon from '@/components/ui/icon'; @@ -89,7 +91,11 @@ const PreviewCard: React.FC = ({ }; const renderVideo = () => { - const content = { __html: addAutoPlay(card.html) }; + const sanitized = DOMPurify.sanitize(addAutoPlay(card.html), { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['allowfullscreen', 'frameborder', 'allow', 'src', 'width', 'height'], + }); + const content = { __html: sanitized }; const ratio = getRatio(card); const height = width / ratio; diff --git a/packages/pl-fe/src/components/scrollable-list.tsx b/packages/pl-fe/src/components/scrollable-list.tsx index aca638312..d61841ca0 100644 --- a/packages/pl-fe/src/components/scrollable-list.tsx +++ b/packages/pl-fe/src/components/scrollable-list.tsx @@ -109,7 +109,13 @@ const ScrollableList = React.forwardRef(({ // Preserve scroll position const scrollDataKey = `plfe:scrollData:${scrollKey}:${locationState.key}`; - const scrollData: SavedScrollPosition | null = useMemo(() => JSON.parse(sessionStorage.getItem(scrollDataKey)!), [scrollDataKey]); + const scrollData: SavedScrollPosition | null = useMemo(() => { + try { + return JSON.parse(sessionStorage.getItem(scrollDataKey)!); + } catch { + return null; + } + }, [scrollDataKey]); const topIndex = useRef(scrollData ? scrollData.index : 0); const topOffset = useRef(scrollData ? scrollData.offset : 0); diff --git a/packages/pl-fe/src/pages/utils/about.tsx b/packages/pl-fe/src/pages/utils/about.tsx index 6a328385a..6dc9a8fd9 100644 --- a/packages/pl-fe/src/pages/utils/about.tsx +++ b/packages/pl-fe/src/pages/utils/about.tsx @@ -1,3 +1,4 @@ +import DOMPurify from 'isomorphic-dompurify'; import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -57,7 +58,7 @@ const About: React.FC = ({ slug }) => {
- {pageHtml &&
} + {pageHtml &&
} {alsoAvailable}
diff --git a/packages/pl-fe/src/reducers/auth.ts b/packages/pl-fe/src/reducers/auth.ts index 89f91c5cc..4dbe80c76 100644 --- a/packages/pl-fe/src/reducers/auth.ts +++ b/packages/pl-fe/src/reducers/auth.ts @@ -83,19 +83,23 @@ const NAMESPACE = trim(BuildConfig.FE_SUBDIRECTORY, '/') ? `pl-fe@${BuildConfig. const STORAGE_KEY = buildKey([NAMESPACE, 'auth']); const getLocalState = (): State | undefined => { - const state = JSON.parse(localStorage.getItem(STORAGE_KEY)!); + try { + const state = JSON.parse(localStorage.getItem(STORAGE_KEY)!); - if (!state) return undefined; + if (!state) return undefined; - return ({ - app: state.app && v.parse(applicationSchema, state.app), - tokens: Object.fromEntries(Object.entries(state.tokens).map(([key, value]) => [key, v.parse(tokenWithAppSchema, value)])), - users: Object.fromEntries(Object.entries(state.users).map(([key, value]) => [key, v.parse(authUserSchema, value)])), - me: state.me, - client: new PlApiClient(parseBaseURL(state.me) || backendUrl, state.users[state.me]?.access_token, { - instance, - }), - }); + return ({ + app: state.app && v.parse(applicationSchema, state.app), + tokens: Object.fromEntries(Object.entries(state.tokens).map(([key, value]) => [key, v.parse(tokenWithAppSchema, value)])), + users: Object.fromEntries(Object.entries(state.users).map(([key, value]) => [key, v.parse(authUserSchema, value)])), + me: state.me, + client: new PlApiClient(parseBaseURL(state.me) || backendUrl, state.users[state.me]?.access_token, { + instance, + }), + }); + } catch { + return undefined; + } }; const localState = getLocalState(); diff --git a/packages/pl-fe/src/utils/redirect.ts b/packages/pl-fe/src/utils/redirect.ts index e344c434c..671860ed5 100644 --- a/packages/pl-fe/src/utils/redirect.ts +++ b/packages/pl-fe/src/utils/redirect.ts @@ -4,11 +4,16 @@ const LOCAL_STORAGE_REDIRECT_KEY = 'plfe:redirect_uri'; const getRedirectUrl = () => { let redirectUri = localStorage.getItem(LOCAL_STORAGE_REDIRECT_KEY); + localStorage.removeItem(LOCAL_STORAGE_REDIRECT_KEY); + if (redirectUri) { redirectUri = decodeURIComponent(redirectUri); + // Only allow relative paths (not protocol-relative like //evil.com) + if (!redirectUri.startsWith('/') || redirectUri.startsWith('//')) { + return '/'; + } } - localStorage.removeItem(LOCAL_STORAGE_REDIRECT_KEY); return redirectUri || '/'; };