Commit 626dadb5 by CaIon

fix(web): secure rich content rendering

parent 0b48ad86
...@@ -16,6 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,6 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import DOMPurify from 'dompurify'
import { useMemo } from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
interface HtmlContentProps { interface HtmlContentProps {
...@@ -24,10 +27,16 @@ interface HtmlContentProps { ...@@ -24,10 +27,16 @@ interface HtmlContentProps {
} }
export function HtmlContent(props: HtmlContentProps) { export function HtmlContent(props: HtmlContentProps) {
const html = useMemo(() => DOMPurify.sanitize(props.content), [props.content])
return ( return (
<div <div
className={cn('prose prose-neutral dark:prose-invert max-w-none', props.className)} className={cn(
dangerouslySetInnerHTML={{ __html: props.content }} 'prose prose-neutral dark:prose-invert max-w-none',
props.className
)}
// eslint-disable-next-line react/no-danger -- html is sanitized above
dangerouslySetInnerHTML={{ __html: html }}
/> />
) )
} }
...@@ -16,18 +16,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,18 +16,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { isLikelyHtml } from '@/lib/content-format'
import { HtmlContent } from '@/components/html-content' import { HtmlContent } from '@/components/html-content'
import { Markdown } from '@/components/ui/markdown' import { Markdown } from '@/components/ui/markdown'
type RichContentMode = 'markdown' | 'html'
interface RichContentProps { interface RichContentProps {
content: string content: string
mode?: RichContentMode
breaks?: boolean breaks?: boolean
className?: string className?: string
} }
export function RichContent(props: RichContentProps) { export function RichContent(props: RichContentProps) {
if (isLikelyHtml(props.content)) { if (props.mode === 'html') {
return <HtmlContent content={props.content} className={props.className} /> return <HtmlContent content={props.content} className={props.className} />
} }
......
...@@ -19,10 +19,12 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,10 +19,12 @@ For commercial licensing, please contact support@quantumnous.com
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Construction } from 'lucide-react' import { Construction } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PublicLayout } from '@/components/layout'
import { RichContent } from '@/components/rich-content' import { RichContent } from '@/components/rich-content'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { PublicLayout } from '@/components/layout' import { isHttpUrl, isLikelyHtml } from '@/lib/content-format'
import { isHttpUrl } from '@/lib/content-format'
import { getAboutContent } from './api' import { getAboutContent } from './api'
function EmptyAboutState() { function EmptyAboutState() {
...@@ -159,6 +161,7 @@ export function About() { ...@@ -159,6 +161,7 @@ export function About() {
<PublicLayout> <PublicLayout>
<div className='mx-auto max-w-6xl px-4 py-8'> <div className='mx-auto max-w-6xl px-4 py-8'>
<RichContent <RichContent
mode={isLikelyHtml(rawContent) ? 'html' : 'markdown'}
content={rawContent} content={rawContent}
className='prose-neutral dark:prose-invert max-w-none' className='prose-neutral dark:prose-invert max-w-none'
/> />
......
...@@ -17,10 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,10 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store'
import { RichContent } from '@/components/rich-content'
import { PublicLayout } from '@/components/layout' import { PublicLayout } from '@/components/layout'
import { Footer } from '@/components/layout/components/footer' import { Footer } from '@/components/layout/components/footer'
import { RichContent } from '@/components/rich-content'
import { isLikelyHtml } from '@/lib/content-format'
import { useAuthStore } from '@/stores/auth-store'
import { CTA, Features, Hero, HowItWorks, Stats } from './components' import { CTA, Features, Hero, HowItWorks, Stats } from './components'
import { useHomePageContent } from './hooks' import { useHomePageContent } from './hooks'
...@@ -57,7 +60,11 @@ export function Home() { ...@@ -57,7 +60,11 @@ export function Home() {
return ( return (
<PublicLayout> <PublicLayout>
<div className='mx-auto max-w-6xl px-4 py-8'> <div className='mx-auto max-w-6xl px-4 py-8'>
<RichContent content={content} className='custom-home-content' /> <RichContent
mode={isLikelyHtml(content) ? 'html' : 'markdown'}
content={content}
className='custom-home-content'
/>
</div> </div>
</PublicLayout> </PublicLayout>
) )
......
...@@ -19,12 +19,14 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,12 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { FileWarning } from 'lucide-react' import { FileWarning } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PublicLayout } from '@/components/layout'
import { RichContent } from '@/components/rich-content' import { RichContent } from '@/components/rich-content'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { PublicLayout } from '@/components/layout' import { isHttpUrl, isLikelyHtml } from '@/lib/content-format'
import { isHttpUrl } from '@/lib/content-format'
import type { LegalDocumentResponse } from './types' import type { LegalDocumentResponse } from './types'
type LegalDocumentProps = { type LegalDocumentProps = {
...@@ -127,6 +129,7 @@ export function LegalDocument({ ...@@ -127,6 +129,7 @@ export function LegalDocument({
</div> </div>
<RichContent <RichContent
mode={isLikelyHtml(rawContent) ? 'html' : 'markdown'}
content={rawContent} content={rawContent}
className='prose-neutral dark:prose-invert max-w-none' className='prose-neutral dark:prose-invert max-w-none'
/> />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment