import { describe, expect, it } from 'bun:test' import { sanitizeTeamsHtml } from './html-sanitizer' describe('sanitizeTeamsHtml', () => { it('keeps a Teams mention tag with its id attribute', () => { expect(sanitizeTeamsHtml('Hey John')).toBe('Hey John') }) it('keeps whitelisted formatting tags', () => { expect(sanitizeTeamsHtml('bold')).toBe('bold') }) it('escapes a disallowed tag with a dangerous attribute', () => { expect(sanitizeTeamsHtml('')).toBe('<img src=x onerror=alert(1)>') }) it('escapes script tags entirely', () => { expect(sanitizeTeamsHtml('')).toBe('<script>alert(1)</script>') }) it('keeps a whitelisted tag but drops its disallowed attribute', () => { expect(sanitizeTeamsHtml('x')).toBe('x') }) it('drops an unsafe href but keeps the anchor tag', () => { expect(sanitizeTeamsHtml('x')).toBe('x') }) it('escapes a bare < that is not part of a recognized tag', () => { expect(sanitizeTeamsHtml('a < b')).toBe('a < b') }) describe('bypass attempts', () => { it('blocks case-variant script tags', () => { expect(sanitizeTeamsHtml('')).toBe('<ScRiPt>x</ScRiPt>') }) it('recognizes a whitelisted tag regardless of case', () => { expect(sanitizeTeamsHtml('John')).toBe('John') }) it('blocks a tag name followed directly by non-whitespace garbage', () => { const result = sanitizeTeamsHtml('') expect(result).not.toContain(' { const result = sanitizeTeamsHtml('alert(1)') expect(result).not.toContain(' { expect(sanitizeTeamsHtml('John')).toBe('John') }) it('drops an unquoted dangerous attribute alongside an allowed one', () => { expect(sanitizeTeamsHtml('John')).toBe('John') }) it('never produces a live tag from nested/malformed markup', () => { const result = sanitizeTeamsHtml('<')).toBe('<script>alert(1)</script>') }) it('does not double-escape already-escaped content', () => { expect(sanitizeTeamsHtml('<script>')).toBe('<script>') }) it('accepts void/self-closing br in any spelling', () => { expect(sanitizeTeamsHtml('
')).toBe('
') expect(sanitizeTeamsHtml('
')).toBe('
') expect(sanitizeTeamsHtml('
')).toBe('
') }) it('allows an https href', () => { expect(sanitizeTeamsHtml('link')).toBe('link') }) it('drops a javascript: href', () => { expect(sanitizeTeamsHtml('link')).toBe('link') }) it('drops a javascript: href even with leading whitespace before it', () => { expect(sanitizeTeamsHtml('link')).toBe('link') }) it('escapes hyphenated custom elements instead of rewriting them to a shorter allowed tag', () => { // Without a hyphen in the tag-name pattern, "" matched as "" plus // leftover text, silently rewriting an unknown element into an anchor. expect(sanitizeTeamsHtml('hi')).toBe('<a-b onclick="x">hi</a-b>') expect(sanitizeTeamsHtml('x')).toBe('<my-widget>x</my-widget>') }) it('escapes tag names that merely start with an allowed name', () => { // A tag name must end at a real boundary. Otherwise "" matched the // "at" prefix and was promoted into a Teams mention, and "" into // bold — arbitrary markup turning into formatting or a mention. expect(sanitizeTeamsHtml('x')).toBe('<at:id>x</at:id>') expect(sanitizeTeamsHtml('x')).toBe('<b_extra>x</b_extra>') expect(sanitizeTeamsHtml('x')).toBe('<b.bold>x</b.bold>') expect(sanitizeTeamsHtml('x')).toBe( '<a:link href="https://evil.com">x</a:link>', ) }) it('ignores attribute names that merely end with an allowed name', () => { // An attribute name must start at a real boundary. Otherwise "foo.id" // was truncated to "id" and "foo.href" to "href", letting arbitrary // attributes forge a mention or a link. expect(sanitizeTeamsHtml('J')).toBe('J') expect(sanitizeTeamsHtml('J')).toBe('J') expect(sanitizeTeamsHtml('x')).toBe('x') }) }) })