patch XSS, injection vuln

This commit is contained in:
2026-02-14 16:30:21 +00:00
parent d89b08c58f
commit 7c87edc28a
6 changed files with 44 additions and 16 deletions

View File

@ -73,7 +73,13 @@ const externalLogin = (host: string) => {
const loginWithCode = (code: string) => const loginWithCode = (code: string) =>
(dispatch: AppDispatch) => { (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 { client_id, client_secret, redirect_uri } = app;
const baseURL = localStorage.getItem('plfe:external:baseurl')!; const baseURL = localStorage.getItem('plfe:external:baseurl')!;
const scope = localStorage.getItem('plfe:external:scopes')!; const scope = localStorage.getItem('plfe:external:scopes')!;

View File

@ -1,10 +1,12 @@
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import clsx from 'clsx'; import clsx from 'clsx';
import DOMPurify from 'isomorphic-dompurify';
import { type MediaAttachment, type PreviewCard as CardEntity, mediaAttachmentSchema } from 'pl-api'; import { type MediaAttachment, type PreviewCard as CardEntity, mediaAttachmentSchema } from 'pl-api';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import * as v from 'valibot'; import * as v from 'valibot';
import Blurhash from '@/components/blurhash'; import Blurhash from '@/components/blurhash';
import HStack from '@/components/ui/hstack'; import HStack from '@/components/ui/hstack';
import Icon from '@/components/ui/icon'; import Icon from '@/components/ui/icon';
@ -89,7 +91,11 @@ const PreviewCard: React.FC<IPreviewCard> = ({
}; };
const renderVideo = () => { 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 ratio = getRatio(card);
const height = width / ratio; const height = width / ratio;

View File

@ -109,7 +109,13 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
// Preserve scroll position // Preserve scroll position
const scrollDataKey = `plfe:scrollData:${scrollKey}:${locationState.key}`; 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<number>(scrollData ? scrollData.index : 0); const topIndex = useRef<number>(scrollData ? scrollData.index : 0);
const topOffset = useRef<number>(scrollData ? scrollData.offset : 0); const topOffset = useRef<number>(scrollData ? scrollData.offset : 0);

View File

@ -1,3 +1,4 @@
import DOMPurify from 'isomorphic-dompurify';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -57,7 +58,7 @@ const About: React.FC<IAbout> = ({ slug }) => {
<div> <div>
<Card variant='rounded'> <Card variant='rounded'>
<div className='prose mx-auto py-4 dark:prose-invert sm:p-6'> <div className='prose mx-auto py-4 dark:prose-invert sm:p-6'>
{pageHtml && <div dangerouslySetInnerHTML={{ __html: pageHtml }} />} {pageHtml && <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(pageHtml, { USE_PROFILES: { html: true } }) }} />}
{alsoAvailable} {alsoAvailable}
</div> </div>
</Card> </Card>

View File

@ -83,19 +83,23 @@ const NAMESPACE = trim(BuildConfig.FE_SUBDIRECTORY, '/') ? `pl-fe@${BuildConfig.
const STORAGE_KEY = buildKey([NAMESPACE, 'auth']); const STORAGE_KEY = buildKey([NAMESPACE, 'auth']);
const getLocalState = (): State | undefined => { 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 ({ return ({
app: state.app && v.parse(applicationSchema, state.app), app: state.app && v.parse(applicationSchema, state.app),
tokens: Object.fromEntries(Object.entries(state.tokens).map(([key, value]) => [key, v.parse(tokenWithAppSchema, value)])), 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)])), users: Object.fromEntries(Object.entries(state.users).map(([key, value]) => [key, v.parse(authUserSchema, value)])),
me: state.me, me: state.me,
client: new PlApiClient(parseBaseURL(state.me) || backendUrl, state.users[state.me]?.access_token, { client: new PlApiClient(parseBaseURL(state.me) || backendUrl, state.users[state.me]?.access_token, {
instance, instance,
}), }),
}); });
} catch {
return undefined;
}
}; };
const localState = getLocalState(); const localState = getLocalState();

View File

@ -4,11 +4,16 @@ const LOCAL_STORAGE_REDIRECT_KEY = 'plfe:redirect_uri';
const getRedirectUrl = () => { const getRedirectUrl = () => {
let redirectUri = localStorage.getItem(LOCAL_STORAGE_REDIRECT_KEY); let redirectUri = localStorage.getItem(LOCAL_STORAGE_REDIRECT_KEY);
localStorage.removeItem(LOCAL_STORAGE_REDIRECT_KEY);
if (redirectUri) { if (redirectUri) {
redirectUri = decodeURIComponent(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 || '/'; return redirectUri || '/';
}; };