Add initial project files
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { safeHref } from '#web-runtime/components/links';
|
||||
import type { NavigationLink } from '#web-runtime/types';
|
||||
|
||||
/**
|
||||
* `safeHref` is the gate between CMS-authored link data and the `href`
|
||||
* attribute the browser receives, so these tests pin the allowlist hard:
|
||||
* anything not explicitly safe must come back as `null`.
|
||||
*/
|
||||
function link(overrides: Partial<NavigationLink> = {}): NavigationLink {
|
||||
return {
|
||||
linkType: 'external',
|
||||
pageId: '',
|
||||
externalUrl: '',
|
||||
openInNewTab: false,
|
||||
label: 'A link',
|
||||
variant: 'primary',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('allowed protocols', () => {
|
||||
it.each([
|
||||
'http://example.com/page',
|
||||
'https://example.com/page?q=1#frag',
|
||||
'mailto:hello@example.com',
|
||||
'tel:+380441234567'
|
||||
])('passes %s through untouched', (href) => {
|
||||
expect(safeHref(link({ href }))).toBe(href);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dangerous protocols', () => {
|
||||
it.each([
|
||||
'javascript:alert(1)',
|
||||
'data:text/html,<script>alert(1)</script>',
|
||||
'vbscript:msgbox(1)',
|
||||
'file:///etc/passwd',
|
||||
'blob:https://example.com/uuid',
|
||||
'ftp://example.com/file'
|
||||
])('rejects %s', (href) => {
|
||||
expect(safeHref(link({ href }))).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects javascript: regardless of letter case', () => {
|
||||
expect(safeHref(link({ href: 'JavaScript:alert(1)' }))).toBeNull();
|
||||
expect(safeHref(link({ href: 'JAVASCRIPT:alert(1)' }))).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects javascript: hidden behind leading whitespace, which URL parsing strips', () => {
|
||||
expect(safeHref(link({ href: ' javascript:alert(1)' }))).toBeNull();
|
||||
expect(safeHref(link({ href: '\njavascript:alert(1)' }))).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects a dangerous externalUrl the same way as a dangerous href', () => {
|
||||
expect(safeHref(link({ externalUrl: 'javascript:alert(1)' }))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('malformed and missing values', () => {
|
||||
it('rejects plain text that is not a URL at all', () => {
|
||||
expect(safeHref(link({ href: 'not a url' }))).toBeNull();
|
||||
expect(safeHref(link({ href: 'example.com/no-protocol' }))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when neither href nor externalUrl is set', () => {
|
||||
expect(safeHref(link())).toBeNull();
|
||||
expect(safeHref(link({ href: '' }))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('href source precedence', () => {
|
||||
it('prefers href over externalUrl when both exist', () => {
|
||||
expect(
|
||||
safeHref(
|
||||
link({ href: 'https://primary.example.com', externalUrl: 'https://other.example.com' })
|
||||
)
|
||||
).toBe('https://primary.example.com');
|
||||
});
|
||||
|
||||
it('falls back to externalUrl when href is empty', () => {
|
||||
expect(safeHref(link({ href: '', externalUrl: 'https://fallback.example.com' }))).toBe(
|
||||
'https://fallback.example.com'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('anchors and relative paths', () => {
|
||||
it('passes in-page anchors through untouched', () => {
|
||||
expect(safeHref(link({ href: '#pricing' }))).toBe('#pricing');
|
||||
});
|
||||
|
||||
it('routes site-relative paths through toPublicHref, which keeps the locale prefix in this multi-locale config', () => {
|
||||
// site.config.ts declares en/uk/de, so localePrefix is 'always' and
|
||||
// toPublicHref is the identity function.
|
||||
expect(safeHref(link({ href: '/en/about' }))).toBe('/en/about');
|
||||
expect(safeHref(link({ href: '/pricing' }))).toBe('/pricing');
|
||||
});
|
||||
|
||||
it('currently lets protocol-relative URLs through via the "/" branch (pinned behavior)', () => {
|
||||
// '//evil.example.com' starts with '/', so it never reaches the protocol
|
||||
// allowlist. Browsers resolve it against the current scheme (http/https),
|
||||
// so it cannot smuggle javascript:, but it is an external URL in disguise.
|
||||
expect(safeHref(link({ href: '//evil.example.com' }))).toBe('//evil.example.com');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user