fix(security,notifications): pre class allowlist, notification privacy + icon, sync-script safety (N100/N106/N109/N119)

- N100: restrict <pre> classes to language-* in sanitize-html allowedClasses;
  previously `class` was allowed on <pre> with no allowedClasses entry, so a
  remote sender could inject arbitrary class names that activate site CSS.
- N106: OS notifications for E2EE rooms no longer carry decrypted plaintext
  (which persists in the OS notification center / lock screen). Encrypted rooms
  show only the sender; the in-page toast still previews while focused.
- N109: OS notification icon/badge use the static app logo instead of an
  authenticated-media avatar URL the OS can't fetch (was 401 / no icon). The
  in-app toast keeps the real room avatar (it can fetch via the SW).
- N119: syncDecorations.mjs distinguishes a confirmed 404 (remove) from a
  network/5xx failure (abort) so a transient CDN outage can't silently wipe the
  whole decoration catalog from source control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:35:33 -04:00
parent 1c84556600
commit 51d468fbcc
3 changed files with 41 additions and 5 deletions
+13 -3
View File
@@ -242,6 +242,7 @@ function MessageNotifications() {
roomId,
eventId,
body,
encrypted,
}: {
roomName: string;
roomAvatar?: string;
@@ -249,6 +250,7 @@ function MessageNotifications() {
roomId: string;
eventId: string;
body?: string;
encrypted?: boolean;
}) => {
const roomPath = mDirects.has(roomId)
? getDirectRoomPath(roomId, eventId)
@@ -267,10 +269,17 @@ function MessageNotifications() {
return;
}
// N109: the OS notification subsystem fetches icon/badge OUTSIDE the page,
// so the SW can't inject auth headers and authenticated-media URLs 401.
// Use the static app logo (as invite notifications already do).
// N106: never put decrypted E2EE plaintext into the OS notification (it
// persists in the notification center / lock screen / is readable by other
// apps). For encrypted rooms show only the sender; the in-page toast above
// still shows the preview while the user is actively looking at the screen.
const noti = new window.Notification(roomName, {
icon: roomAvatar,
badge: roomAvatar,
body: body ? `${username}: ${body}`.slice(0, 120) : username,
icon: LogoSVG,
badge: LogoSVG,
body: !encrypted && body ? `${username}: ${body}`.slice(0, 120) : username,
silent: true,
});
@@ -341,6 +350,7 @@ function MessageNotifications() {
roomId: room.roomId,
eventId,
body: (mEvent.getContent().body as string | undefined) ?? '',
encrypted: room.hasEncryptionStateEvent(),
});
}
+5
View File
@@ -155,6 +155,11 @@ export const sanitizeCustomHtml = (customHtml: string): string =>
allowProtocolRelative: false,
allowedClasses: {
code: ['language-*'],
// `pre` permits `class` (for `<pre class="language-*">` wrappers); without
// an allowedClasses entry, sanitize-html lets a remote sender put ARBITRARY
// class names on <pre>, activating site CSS (N100). Restrict to the same
// language-* whitelist as <code>.
pre: ['language-*'],
},
allowedStyles: {
'*': {