Set up Vitest with server (node) and client (jsdom) test projects. 79 tests across 8 files verify all Phase 0-1 security controls: escapeHtml sanitization, DOMPurify markdown XSS prevention, CSRF enforcement, security headers, rate limiting, auth guards, profile field allowlist, and login anti-enumeration. Updated SECURITY_EVALUATION.md with remediation status, implementation summary, and automated test coverage details.
110 lines
3.4 KiB
JavaScript
110 lines
3.4 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import { useMarkdown } from '../../../app/composables/useMarkdown.js'
|
|
|
|
describe('useMarkdown', () => {
|
|
const { render } = useMarkdown()
|
|
|
|
describe('XSS prevention', () => {
|
|
it('strips script tags', () => {
|
|
const result = render('Hello <script>alert("xss")</script> world')
|
|
expect(result).not.toContain('<script>')
|
|
expect(result).not.toContain('</script>')
|
|
expect(result).toContain('Hello')
|
|
expect(result).toContain('world')
|
|
})
|
|
|
|
it('strips onerror attributes', () => {
|
|
const result = render('<img onerror="alert(1)" src="x">')
|
|
expect(result).not.toContain('onerror')
|
|
})
|
|
|
|
it('strips onclick attributes', () => {
|
|
const result = render('<a onclick="alert(1)" href="#">click</a>')
|
|
expect(result).not.toContain('onclick')
|
|
})
|
|
|
|
it('strips iframe tags', () => {
|
|
const result = render('<iframe src="https://evil.com"></iframe>')
|
|
expect(result).not.toContain('<iframe')
|
|
})
|
|
|
|
it('strips object tags', () => {
|
|
const result = render('<object data="exploit.swf"></object>')
|
|
expect(result).not.toContain('<object')
|
|
})
|
|
|
|
it('strips embed tags', () => {
|
|
const result = render('<embed src="exploit.swf">')
|
|
expect(result).not.toContain('<embed')
|
|
})
|
|
|
|
it('sanitizes javascript: URIs', () => {
|
|
const result = render('[click me](javascript:alert(1))')
|
|
expect(result).not.toContain('javascript:')
|
|
})
|
|
|
|
it('strips img tags (not in allowed list)', () => {
|
|
const result = render('')
|
|
expect(result).not.toContain('<img')
|
|
})
|
|
})
|
|
|
|
describe('preserves safe markdown', () => {
|
|
it('renders bold and italic', () => {
|
|
const result = render('**bold** and *italic*')
|
|
expect(result).toContain('<strong>bold</strong>')
|
|
expect(result).toContain('<em>italic</em>')
|
|
})
|
|
|
|
it('renders links with href', () => {
|
|
const result = render('[Ghost Guild](https://ghostguild.org)')
|
|
expect(result).toContain('<a')
|
|
expect(result).toContain('href="https://ghostguild.org"')
|
|
})
|
|
|
|
it('preserves headings h1-h6', () => {
|
|
for (let i = 1; i <= 6; i++) {
|
|
const hashes = '#'.repeat(i)
|
|
const result = render(`${hashes} Heading ${i}`)
|
|
expect(result).toContain(`<h${i}>`)
|
|
}
|
|
})
|
|
|
|
it('preserves code blocks', () => {
|
|
const result = render('`inline code` and\n\n```\nblock code\n```')
|
|
expect(result).toContain('<code>')
|
|
expect(result).toContain('<pre>')
|
|
})
|
|
|
|
it('preserves blockquotes', () => {
|
|
const result = render('> This is a quote')
|
|
expect(result).toContain('<blockquote>')
|
|
})
|
|
|
|
it('preserves lists', () => {
|
|
const result = render('- item 1\n- item 2')
|
|
expect(result).toContain('<ul>')
|
|
expect(result).toContain('<li>')
|
|
})
|
|
|
|
it('preserves allowed attributes: href, target, rel, class', () => {
|
|
// DOMPurify allows href on <a> tags
|
|
const result = render('[link](https://example.com)')
|
|
expect(result).toContain('href=')
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('returns empty string for null', () => {
|
|
expect(render(null)).toBe('')
|
|
})
|
|
|
|
it('returns empty string for undefined', () => {
|
|
expect(render(undefined)).toBe('')
|
|
})
|
|
|
|
it('returns empty string for empty string', () => {
|
|
expect(render('')).toBe('')
|
|
})
|
|
})
|
|
})
|