diff --git a/client/src/modules/TeamForumPanel.jsx b/client/src/modules/TeamForumPanel.jsx index d961654..a823bdb 100644 --- a/client/src/modules/TeamForumPanel.jsx +++ b/client/src/modules/TeamForumPanel.jsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' +import DOMPurify from 'dompurify' import { api } from '../api/client.js' import { useAuth } from '../contexts/AuthContext.jsx' import { useSite } from '../contexts/SiteContext.jsx' @@ -118,7 +119,7 @@ export default function TeamForumPanel({ externalId, moduleId }) { Announcements {forum.canPost && !composing && ( - )} @@ -225,7 +226,7 @@ function GuestManager({ slug }) { if (!open) { return ( - ) @@ -235,7 +236,7 @@ function GuestManager({ slug }) {

Forum guests

- +

Guests read and post in this forum without being members of the Team. They do not appear on the @@ -247,7 +248,7 @@ function GuestManager({ slug }) { {(data?.guests || []).map((g) => (

  • {g.username} - +
  • ))} {data && data.guests.length === 0 && ( @@ -257,14 +258,14 @@ function GuestManager({ slug }) {
    setUsername(e.target.value)} placeholder="Account name" maxLength={32} required /> - +
    {error &&

    {error}

    }
    @@ -274,7 +275,7 @@ function GuestManager({ slug }) { function ThreadView({ thread, canModerate, onBack, onModerate }) { return (
    -

    @@ -288,24 +289,33 @@ function ThreadView({ thread, canModerate, onBack, onModerate }) { {thread.posts.map((post) => (
    {/* - Rendered server-side under the operator's image policy, which is why - this is dangerouslySetInnerHTML and not a sanitizer call here. The body - was sanitised on write with the forum's own profile — one in which - `img` is never allowed — and any in it was emitted by core's own - renderer with a fixed attribute set. A client-side sanitiser would have - to strip exactly the tag core just decided to add. + Sanitised on write with the forum's own profile, rendered server-side + under the operator's image policy, and re-sanitised here — the same + defence-in-depth every other body-HTML surface on this site applies + (FiveOnFriday, NewsletterIssue, the rich-text block). + + `ADD_ATTR: ['referrerpolicy']` is load-bearing and not a preference. + DOMPurify's default allowlist carries `loading` but NOT + `referrerpolicy`, so a plain sanitize() call silently strips the one + attribute that limits what a remote embed leaks to the host serving it + — the privacy property the admin help text promises an operator. The + itself is core's own output with a fixed attribute set, so + nothing here is widening what an author can write. */} {/* eslint-disable-next-line react/no-danger */} -
    +
    ))} {canModerate && (
    - -
    @@ -352,7 +362,7 @@ function Composer({ slug, imageMode, onCancel, onPosted }) { return (
    setTitle(e.target.value)} placeholder="Title" @@ -360,7 +370,7 @@ function Composer({ slug, imageMode, onCancel, onPosted }) { required />