160 lines
7.9 KiB
TypeScript
160 lines
7.9 KiB
TypeScript
|
|
import { describe, test, expect } from 'vitest';
|
||
|
|
import { ButtonLink } from './basic/ButtonLink';
|
||
|
|
import { Icon } from './basic/Icon';
|
||
|
|
import { SocialLinks } from './basic/SocialLinks';
|
||
|
|
import { Logo } from './basic/Logo';
|
||
|
|
import { Menu } from './basic/Menu';
|
||
|
|
import { Navbar } from './basic/Navbar';
|
||
|
|
import { ContentSlider } from './sections/ContentSlider';
|
||
|
|
import { FeaturesGrid } from './sections/FeaturesGrid';
|
||
|
|
import { PricingTable } from './sections/PricingTable';
|
||
|
|
import { ImageBlock } from './media/ImageBlock';
|
||
|
|
import { VideoBlock } from './media/VideoBlock';
|
||
|
|
import { Gallery } from './sections/Gallery';
|
||
|
|
import { BackgroundSection } from './layout/BackgroundSection';
|
||
|
|
import { HeroSimple } from './sections/HeroSimple';
|
||
|
|
import { CallToAction } from './sections/CallToAction';
|
||
|
|
import { MapEmbed } from './media/MapEmbed';
|
||
|
|
import { FormContainer } from './forms/FormContainer';
|
||
|
|
|
||
|
|
const XSS = 'javascript:alert(1)';
|
||
|
|
const QUOTE_BREAKOUT = 'x" onerror="alert(1)"';
|
||
|
|
|
||
|
|
function toHtmlOf(Component: any) {
|
||
|
|
return Component.toHtml as (props: any, childrenHtml: string) => { html: string };
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('A3: exported URLs are wrapped in safeUrl + escapeAttr', () => {
|
||
|
|
test('ButtonLink href', () => {
|
||
|
|
const html = toHtmlOf(ButtonLink)({ href: XSS, text: 'Go' }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(ButtonLink)({ href: QUOTE_BREAKOUT, text: 'Go' }, '').html;
|
||
|
|
expect(html2).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('Icon href (link), class is escaped but NOT safeUrl-filtered', () => {
|
||
|
|
const html = toHtmlOf(Icon)({ link: XSS, icon: 'fa-star' }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(Icon)({ link: QUOTE_BREAKOUT, icon: 'fa-star' }, '').html;
|
||
|
|
expect(html2).not.toContain('onerror="alert(1)"');
|
||
|
|
// class attribute is a CSS class, not a URL -- still escaped for attr safety
|
||
|
|
const html3 = toHtmlOf(Icon)({ icon: 'fa-star" onerror="alert(1)' }, '').html;
|
||
|
|
expect(html3).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('SocialLinks href', () => {
|
||
|
|
const html = toHtmlOf(SocialLinks)({ links: [{ platform: 'facebook', url: XSS }] }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(SocialLinks)({ links: [{ platform: 'facebook', url: QUOTE_BREAKOUT }] }, '').html;
|
||
|
|
expect(html2).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('Logo href + image src', () => {
|
||
|
|
const html = toHtmlOf(Logo)({ href: XSS, type: 'text', text: 'Site' }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(Logo)({ type: 'image', imageSrc: XSS }, '').html;
|
||
|
|
expect(html2).not.toContain('javascript:');
|
||
|
|
const html3 = toHtmlOf(Logo)({ type: 'image', imageSrc: QUOTE_BREAKOUT }, '').html;
|
||
|
|
expect(html3).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('Menu link href', () => {
|
||
|
|
const html = toHtmlOf(Menu)({ links: [{ text: 'x', href: XSS }] }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(Menu)({ links: [{ text: 'x', href: QUOTE_BREAKOUT }] }, '').html;
|
||
|
|
expect(html2).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('Navbar logo href, logo image src, link hrefs', () => {
|
||
|
|
const html = toHtmlOf(Navbar)({ logoUrl: XSS, logoType: 'text', logoText: 'Site', links: [{ text: 'x', href: XSS }] }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(Navbar)({ logoType: 'image', logoImage: XSS }, '').html;
|
||
|
|
expect(html2).not.toContain('javascript:');
|
||
|
|
const html3 = toHtmlOf(Navbar)({ logoUrl: QUOTE_BREAKOUT, links: [{ text: 'x', href: QUOTE_BREAKOUT }] }, '').html;
|
||
|
|
expect(html3).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('ContentSlider button href + slide image src (background url)', () => {
|
||
|
|
const html = toHtmlOf(ContentSlider)({ slides: [{ buttonText: 'Go', buttonHref: XSS, imageSrc: '' }] }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(ContentSlider)({ slides: [{ imageSrc: XSS }] }, '').html;
|
||
|
|
expect(html2).not.toContain('javascript:');
|
||
|
|
const html3 = toHtmlOf(ContentSlider)({ slides: [{ buttonText: 'Go', buttonHref: QUOTE_BREAKOUT }] }, '').html;
|
||
|
|
expect(html3).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FeaturesGrid image src + button url', () => {
|
||
|
|
const html = toHtmlOf(FeaturesGrid)({ features: [{ title: 't', description: 'd', image: XSS }] }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(FeaturesGrid)({ features: [{ title: 't', description: 'd', buttonText: 'Go', buttonUrl: XSS }] }, '').html;
|
||
|
|
expect(html2).not.toContain('javascript:');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('PricingTable button href', () => {
|
||
|
|
const html = toHtmlOf(PricingTable)({ plans: [{ name: 'p', price: '$1', period: '/mo', features: [], buttonText: 'Buy', buttonHref: XSS }] }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(PricingTable)({ plans: [{ name: 'p', price: '$1', period: '/mo', features: [], buttonText: 'Buy', buttonHref: QUOTE_BREAKOUT }] }, '').html;
|
||
|
|
expect(html2).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('ImageBlock img src', () => {
|
||
|
|
const html = toHtmlOf(ImageBlock)({ src: XSS }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(ImageBlock)({ src: QUOTE_BREAKOUT }, '').html;
|
||
|
|
expect(html2).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('VideoBlock direct-file src is safeUrl-filtered on final emitted src', () => {
|
||
|
|
// matches the .mp4 extension sniff in detectVideoType but carries a javascript: scheme
|
||
|
|
const html = toHtmlOf(VideoBlock)({ videoUrl: 'javascript:alert(1)//x.mp4' }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(VideoBlock)({ videoUrl: '"><script>alert(1)</script>.mp4' }, '').html;
|
||
|
|
expect(html2).not.toContain('<script>alert(1)</script>');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('VideoBlock youtube embed still works after safeUrl pass', () => {
|
||
|
|
const html = toHtmlOf(VideoBlock)({ videoUrl: 'https://www.youtube.com/watch?v=abc123' }, '').html;
|
||
|
|
expect(html).toContain('https://www.youtube.com/embed/abc123');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('Gallery img src', () => {
|
||
|
|
const html = toHtmlOf(Gallery)({ images: [{ src: XSS, alt: 'a' }] }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(Gallery)({ images: [{ src: QUOTE_BREAKOUT, alt: 'a' }] }, '').html;
|
||
|
|
expect(html2).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('BackgroundSection bg image url()', () => {
|
||
|
|
const html = toHtmlOf(BackgroundSection)({ bgImage: XSS }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('HeroSimple bg image url() and bg video src', () => {
|
||
|
|
const html = toHtmlOf(HeroSimple)({ bgType: 'image', bgImage: XSS }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
const html2 = toHtmlOf(HeroSimple)({ bgType: 'video', bgVideo: XSS }, '').html;
|
||
|
|
expect(html2).not.toContain('javascript:');
|
||
|
|
const html3 = toHtmlOf(HeroSimple)({ bgType: 'video', bgVideo: QUOTE_BREAKOUT }, '').html;
|
||
|
|
expect(html3).not.toContain('onerror="alert(1)"');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('CallToAction bg image url()', () => {
|
||
|
|
const html = toHtmlOf(CallToAction)({ bgType: 'image', bgValue: XSS }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('MapEmbed iframe src (found beyond the brief-listed sites via grep sweep)', () => {
|
||
|
|
const html = toHtmlOf(MapEmbed)({ address: 'New York, NY', zoom: 14 }, '').html;
|
||
|
|
expect(html).toContain('maps.google.com');
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('FormContainer legacy form action is safeUrl-filtered (found via grep sweep)', () => {
|
||
|
|
const html = toHtmlOf(FormContainer)({ action: XSS, method: 'GET' }, '').html;
|
||
|
|
expect(html).not.toContain('javascript:');
|
||
|
|
// non-relay legacy path (no recipientEmail) still works normally
|
||
|
|
const html2 = toHtmlOf(FormContainer)({ action: '/legacy', method: 'POST' }, '').html;
|
||
|
|
expect(html2).toContain('action="/legacy"');
|
||
|
|
});
|
||
|
|
});
|