ATProto social network
Something went wrong. Try again.
5.3 kB · 160 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161import { describe, it, expect } from 'vitest';import { render, screen } from '@testing-library/svelte';import TextBlock from './TextBlock.svelte';import type { TextBlock as TextBlockType } from '$lib/types/content-blocks';
/** * Helper: create a link facet covering the byte range of `linkText` within `fullText`. */function makeLinkFacet(fullText: string, linkText: string, uri: string) { const encoder = new TextEncoder(); const fullBytes = encoder.encode(fullText); const linkBytes = encoder.encode(linkText);
// Find byte offset of linkText within fullText const fullStr = new TextDecoder().decode(fullBytes); const charStart = fullStr.indexOf(linkText); const byteStart = encoder.encode(fullStr.slice(0, charStart)).length; const byteEnd = byteStart + linkBytes.length;
return { index: { byteStart, byteEnd }, features: [{ $type: 'app.bsky.richtext.facet#link', uri }] };}
describe('TextBlock XSS sanitisation', () => { describe('dangerous protocol URIs render as plain text', () => { it('does not create an anchor for javascript: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'javascript:alert(1)')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block');
// The text "click here" must appear but NOT inside an <a> tag expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); });
it('does not create an anchor for data: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [ makeLinkFacet( text, 'click here', 'data:text/html,<script>alert(1)</script>' ) ] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block');
expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); });
it('does not create an anchor for vbscript: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'vbscript:msgbox')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block');
expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); });
it('does not create an anchor for mixed-case jAvAsCrIpT: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'JaVaScRiPt:alert(1)')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block');
expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); });
it('does not create an anchor for whitespace-prefixed javascript: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', ' javascript:alert(1)')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block');
expect(el.innerHTML).toContain('click here'); expect(el.querySelector('a')).toBeNull(); }); });
describe('valid http(s) URIs still render as links', () => { it('renders an anchor for https: URIs', () => { const text = 'click here now'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', 'https://example.com')] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block'); const anchor = el.querySelector('a');
expect(anchor).not.toBeNull(); expect(anchor!.getAttribute('href')).toBe('https://example.com'); }); });
describe('escapeAttr escapes & in URIs', () => { it('escapes & characters in href attributes', () => { const text = 'click here now'; const uri = 'https://example.com?a=1&b=2'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', uri)] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block');
// The raw innerHTML should have & not bare & // (bare & in an attribute is technically invalid HTML) expect(el.innerHTML).toContain('href="https://example.com?a=1&b=2"'); }); });
describe('escapeAttr escapes quotes in URIs', () => { it('escapes double-quote characters in href attributes', () => { const text = 'click here now'; const uri = 'https://example.com?a="bad"'; const block: TextBlockType = { $type: 'at.riven.block.text', text, facets: [makeLinkFacet(text, 'click here', uri)] }; render(TextBlock, { props: { block } }); const el = screen.getByTestId('text-block');
// The " in the URI must be escaped as " so it doesn't break out expect(el.innerHTML).toContain('"'); // The anchor should still be present and functional const anchor = el.querySelector('a'); expect(anchor).not.toBeNull(); }); });});