fix(security): validate avatar-decoration slugs from remote profiles
CI / Build & Quality Checks (push) Successful in 1m33s
CI / Trigger Desktop Build (push) Successful in 21s

The MSC4133 io.lotus.avatar_decoration value was interpolated into the
CDN URL verbatim, letting a room member steer the path/query of a request
every viewer's browser makes. Accept only slugs present in the catalog;
anything else is treated as no decoration. Unit-tested.

Fixes #64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-12 14:48:35 -04:00
co-authored by Claude Opus 5
parent cecf65a3a1
commit f3119e3dc2
3 changed files with 36 additions and 1 deletions
+5 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { MatrixError, Method } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
import { isValidDecorationSlug } from '../features/lotus/avatarDecorations';
const PROFILE_FIELD = 'io.lotus.avatar_decoration';
@@ -51,7 +52,10 @@ function fetchDecoration(
// all fields (incl. custom MSC4133 ones); read the decoration out of it.
return authedRequest(Method.Get, `/profile/${encodeURIComponent(userId)}`)
.then((res) => {
const val = (res[PROFILE_FIELD] as string | undefined) ?? null;
const rawVal = (res[PROFILE_FIELD] as string | undefined) ?? null;
// The remote profile field is free-form and attacker-controlled; only
// accept it when it names a real catalog decoration (see decorationUrl).
const val = rawVal && isValidDecorationSlug(rawVal) ? rawVal : null;
cache.set(userId, val);
return val;
})