import { describe, it, expect } from 'vitest'; import { parseSync } from '../../parser/index.js'; import { transformDocument } from '../../renderer/transformer.js'; import { sanitizeUrl } from '../../renderer/sanitize-url.js'; interface RenderedElement { tag?: string; props?: Record; children?: unknown[]; } const findFirst = (nodes: unknown[], tag: string): RenderedElement | null => { for (const node of nodes) { if (node === null || typeof node !== 'object') continue; const element = node as RenderedElement; if (element.tag === tag) return element; if (Array.isArray(element.children)) { const nested = findFirst(element.children, tag); if (nested) return nested; } } return null; }; const renderLinkHref = (markdown: string): string | undefined => { const doc = parseSync(markdown); const out = transformDocument(doc, {}); const anchor = findFirst(out as unknown[], 'a'); return anchor?.props?.['href'] as string | undefined; }; const renderImageSrc = (markdown: string): string | undefined => { const doc = parseSync(markdown); const out = transformDocument(doc, {}); const img = findFirst(out as unknown[], 'img'); return img?.props?.['src'] as string | undefined; }; describe('sanitizeUrl', () => { it('should neutralize javascript: URLs', () => { expect(sanitizeUrl('javascript:alert(1)')).toBe('#'); expect(sanitizeUrl('JavaScript:alert(1)')).toBe('#'); expect(sanitizeUrl(' javascript:alert(1)')).toBe('#'); }); it('should neutralize entity-encoded javascript: URLs', () => { expect(sanitizeUrl('javascript:alert(1)')).toBe('#'); expect(sanitizeUrl('javascript:alert(1)')).toBe('#'); expect(sanitizeUrl('java\tscript:alert(1)')).toBe('#'); expect(sanitizeUrl('java\nscript:alert(1)')).toBe('#'); expect(sanitizeUrl('javascript:alert(1)')).toBe('#'); }); it('should neutralize vbscript:, file:, and data:text/html URLs', () => { expect(sanitizeUrl('vbscript:msgbox(1)')).toBe('#'); expect(sanitizeUrl('file:///etc/passwd')).toBe('#'); expect(sanitizeUrl('data:text/html,')).toBe('#'); }); it('should allow safe absolute and relative URLs unchanged', () => { expect(sanitizeUrl('https://example.com/x?y=1')).toBe( 'https://example.com/x?y=1' ); expect(sanitizeUrl('http://example.com')).toBe('http://example.com'); expect(sanitizeUrl('/docs/getting-started')).toBe( '/docs/getting-started' ); expect(sanitizeUrl('./relative')).toBe('./relative'); expect(sanitizeUrl('#anchor')).toBe('#anchor'); expect(sanitizeUrl('//cdn.example.com/a.js')).toBe( '//cdn.example.com/a.js' ); expect(sanitizeUrl('mailto:hi@example.com')).toBe( 'mailto:hi@example.com' ); expect(sanitizeUrl('tel:+15551234567')).toBe('tel:+15551234567'); }); it('should allow safe data image URLs when images are permitted', () => { const dataImg = 'data:image/png;base64,iVBORw0KGgo='; expect(sanitizeUrl(dataImg, { allowDataImages: true })).toBe(dataImg); expect(sanitizeUrl(dataImg)).toBe('#'); expect( sanitizeUrl('data:image/svg+xml,', { allowDataImages: true, }) ).toBe('#'); }); it('should return the anchor fallback for empty input', () => { expect(sanitizeUrl('')).toBe('#'); expect(sanitizeUrl(' ')).toBe('#'); }); }); describe('Ink link and image URL rendering', () => { it('should not render javascript: links', () => { expect(renderLinkHref('[x](javascript:alert(document.cookie))')).toBe( '#' ); }); it('should not render entity-encoded javascript: links', () => { expect(renderLinkHref('[x](javascript:alert(1))')).toBe('#'); }); it('should render safe links unchanged', () => { expect(renderLinkHref('[x](https://effuse.dev)')).toBe( 'https://effuse.dev' ); expect(renderLinkHref('[x](/docs)')).toBe('/docs'); }); it('should not render javascript: image sources', () => { expect(renderImageSrc('![x](javascript:alert(1))')).toBe('#'); }); it('should allow safe image sources', () => { expect(renderImageSrc('![x](/logo.png)')).toBe('/logo.png'); expect(renderImageSrc('![x](data:image/png;base64,iVBORw0KGgo=)')).toBe( 'data:image/png;base64,iVBORw0KGgo=' ); }); });