fix(collab): sanitize cross-user note markdown and harden note links

This commit is contained in:
Maurice
2026-09-01 14:42:04 +02:00
committed by Maurice
parent 37d7365707
commit 9f6cd3dc6d
3 changed files with 10 additions and 4 deletions
+3 -1
View File
@@ -2,6 +2,8 @@ import { useState, useEffect, useCallback, useMemo } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import rehypeSanitize from 'rehype-sanitize'
import { markdownLinkComponents } from '../shared/markdownLink'
import { createPortal } from 'react-dom'
import { Plus, Pencil, X, StickyNote, Settings } from 'lucide-react'
import { collabApi } from '../../api/client'
@@ -445,7 +447,7 @@ function ViewNoteModal(S: NotesState) {
</div>
</div>
<div className="collab-note-md-full" style={{ padding: '16px 20px', overflowY: 'auto', fontSize: 'calc(14px * var(--fs-scale-body, 1))', color: 'var(--text-primary)', lineHeight: 1.7 }}>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{viewingNote.content || ''}</Markdown>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[rehypeSanitize]} components={markdownLinkComponents}>{viewingNote.content || ''}</Markdown>
{(viewingNote.attachments || []).length > 0 && (
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border-primary)' }}>
<div style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10 }}>{t('files.title')}</div>
@@ -3,6 +3,8 @@ import { avatarSrc } from '../../utils/avatarSrc'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import rehypeSanitize from 'rehype-sanitize'
import { markdownLinkComponents } from '../shared/markdownLink'
import { Trash2, Pin, PinOff, Pencil, Maximize2 } from 'lucide-react'
import { FONT } from './CollabNotes.constants'
import { AuthedImg } from './CollabNotesAuthedImg'
@@ -142,7 +144,7 @@ export function NoteCard({ note, currentUser, canEdit, onUpdate, onDelete, onEdi
maxHeight: '4.5em', overflow: 'hidden',
wordBreak: 'break-word', fontFamily: FONT,
}}>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{note.content}</Markdown>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[rehypeSanitize]} components={markdownLinkComponents}>{note.content}</Markdown>
</div>
)}
</div>
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import rehypeSanitize from 'rehype-sanitize'
import { markdownLinkComponents } from '../../../../components/shared/markdownLink'
import { Check, FileText, Paperclip, Pin, PinOff, Plus, StickyNote, Trash2, X } from 'lucide-react'
import MDancingTrek from '../../../components/MDancingTrek'
import { collabApi } from '../../../../api/client'
@@ -336,7 +338,7 @@ function NoteCardRow({ note, color, canEdit, onTap, onTogglePin, onDelete, t }:
<button type="button" onClick={onTap} className="block w-full px-3 pb-3 pt-[9px] text-left">
{note.content && (
<div className="line-clamp-3 font-geist text-[0.75rem] leading-[1.5] text-m-muted [&_a]:underline [&_ol]:list-decimal [&_ol]:pl-4 [&_p]:mb-1 [&_strong]:font-bold [&_ul]:list-disc [&_ul]:pl-4">
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{note.content}</Markdown>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[rehypeSanitize]} components={markdownLinkComponents}>{note.content}</Markdown>
</div>
)}
<div className={`flex items-center gap-[6px] ${note.content ? 'mt-2' : ''}`}>
@@ -600,7 +602,7 @@ function NoteViewSheet({ open, note, onClose, t }: {
<div className="min-h-0 flex-1 overflow-y-auto px-[18px] pb-4 pt-1">
{snapshot?.content && (
<div className="font-geist text-[0.8125rem] leading-[1.6] text-m-ink [&_a]:underline [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:mb-2 [&_strong]:font-bold [&_ul]:list-disc [&_ul]:pl-5">
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{snapshot.content}</Markdown>
<Markdown remarkPlugins={[remarkGfm, remarkBreaks]} rehypePlugins={[rehypeSanitize]} components={markdownLinkComponents}>{snapshot.content}</Markdown>
</div>
)}
{(snapshot?.attachments.length ?? 0) > 0 && (