55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
import { describe, test, expect } from 'vitest';
|
|||
|
|
import { ButtonLink } from './ButtonLink';
|
||
|
|
|
||
|
|
const toHtml = (ButtonLink as any).toHtml;
|
||
|
|
|
||
|
|
describe('ButtonLink.toHtml href sanitization (attacker-controlled `href` prop)', () => {
|
||
|
|
test('a javascript: URL is neutralized', () => {
|
||
|
|
const { html } = toHtml({ href: 'javascript:alert(1)', text: 'Click' }, '');
|
||
|
|
expect(html).not.toContain('javascript:alert');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a quote-breakout href does not escape the href attribute', () => {
|
||
|
|
const malicious = '"><script>alert(1)</script>';
|
||
|
|
const { html } = toHtml({ href: malicious, text: 'Click' }, '');
|
||
|
|
expect(html).not.toContain('<script>alert(1)</script>');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a normal href still renders correctly', () => {
|
||
|
|
const { html } = toHtml({ href: 'https://example.com', text: 'Click' }, '');
|
||
|
|
expect(html).toContain('href="https://example.com"');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('ButtonLink.toHtml target (boolean-gated, not raw interpolation)', () => {
|
||
|
|
test('an attribute-breakout value for target does not reach the output raw', () => {
|
||
|
|
const malicious = '_blank" onmouseover="alert(1)' as any;
|
||
|
|
const { html } = toHtml({ href: '#', text: 'x', target: malicious }, '');
|
||
|
|
expect(html).not.toContain('onmouseover');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('target="_blank" still adds rel=noopener noreferrer', () => {
|
||
|
|
const { html } = toHtml({ href: '#', text: 'x', target: '_blank' }, '');
|
||
|
|
expect(html).toContain('target="_blank"');
|
||
|
|
expect(html).toContain('rel="noopener noreferrer"');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('ButtonLink.toHtml text escaping (attacker-controlled `text` prop)', () => {
|
||
|
|
test('a tag-breakout attempt in text is neutralized (no injected element)', () => {
|
||
|
|
const { html } = toHtml({ href: '#', text: '</a><img src=x onerror=alert(1)>' }, '');
|
||
|
|
expect(html).not.toContain('<img');
|
||
|
|
expect(html).toContain('<img');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('ampersand is escaped for well-formed text content (consistency with escapeHtml)', () => {
|
||
|
|
const { html } = toHtml({ href: '#', text: 'Tom & Jerry' }, '');
|
||
|
|
expect(html).toContain('Tom & Jerry');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a normal text value still renders unchanged', () => {
|
||
|
|
const { html } = toHtml({ href: '#', text: 'Click Me' }, '');
|
||
|
|
expect(html).toContain('>Click Me</a>');
|
||
|
|
});
|
||
|
|
});
|