@@ -595,6 +595,12 @@ const getSystemTheme = (): Theme => {
595595 return window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ? 'dark' : 'light' ;
596596} ;
597597
598+ // Safely extract text from HTML using DOM (immune to nested tag attacks)
599+ const stripHtmlTags = ( html : string ) : string => {
600+ const doc = new DOMParser ( ) . parseFromString ( html , 'text/html' ) ;
601+ return doc . body . textContent || '' ;
602+ } ;
603+
598604export default function PublicNotePage ( ) {
599605 const [ note , setNote ] = useState < ApiPublicNote | null > ( null ) ;
600606 const [ loading , setLoading ] = useState ( true ) ;
@@ -669,7 +675,7 @@ export default function PublicNotePage() {
669675 content = content . replace ( headingRegex , ( _match , tag , attrs , text ) => {
670676 const level = parseInt ( tag . charAt ( 1 ) ) ;
671677 const id = `heading-${ headingIndex } ` ;
672- const plainText = text . replace ( / < [ ^ > ] * > / g , '' ) ; // Strip HTML tags from heading text
678+ const plainText = stripHtmlTags ( text ) ;
673679 headings . push ( { level, text : plainText , id } ) ;
674680 headingIndex ++ ;
675681 return `<${ tag } ${ attrs } id="${ id } ">${ text } </${ tag } >` ;
@@ -755,7 +761,7 @@ export default function PublicNotePage() {
755761
756762 // Set meta description
757763 const description = note . content
758- ? note . content . replace ( / < [ ^ > ] * > / g , '' ) . slice ( 0 , 160 ) . trim ( ) + '...'
764+ ? stripHtmlTags ( note . content ) . slice ( 0 , 160 ) . trim ( ) + '...'
759765 : 'A note shared via Typelets' ;
760766
761767 let metaDescription = document . querySelector ( 'meta[name="description"]' ) ;
0 commit comments