Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

271 lines
9.3 KiB
JavaScript

/**
* Tests for security/xss-protection.js
*
* Tests HTML escaping, sanitization, and safe DOM manipulation
* functions that protect against XSS attacks.
*/
import '@js/security/xss-protection.js';
const { escapeHtml, escapeHtmlAttribute, sanitizeUserInput,
safeSetInnerHTML, safeSetTextContent } = window;
describe('escapeHtml', () => {
it('escapes ampersands', () => {
expect(escapeHtml('a & b')).toBe('a & b');
});
it('escapes angle brackets', () => {
expect(escapeHtml('<script>alert(1)</script>')).toBe(
'&lt;script&gt;alert(1)&lt;&#x2F;script&gt;'
);
});
it('escapes double quotes', () => {
expect(escapeHtml('"hello"')).toBe('&quot;hello&quot;');
});
it('escapes single quotes', () => {
expect(escapeHtml("it's")).toBe('it&#39;s');
});
it('escapes forward slashes', () => {
expect(escapeHtml('a/b')).toBe('a&#x2F;b');
});
it('returns empty string for null', () => {
expect(escapeHtml(null)).toBe('');
});
it('returns empty string for undefined', () => {
expect(escapeHtml(undefined)).toBe('');
});
it('converts non-strings to string first', () => {
expect(escapeHtml(123)).toBe('123');
expect(escapeHtml(true)).toBe('true');
});
it('handles strings with no special characters', () => {
expect(escapeHtml('hello world')).toBe('hello world');
});
it('handles empty string', () => {
expect(escapeHtml('')).toBe('');
});
it('escapes all dangerous characters together', () => {
expect(escapeHtml('<img onerror="alert(\'xss\')" />')).toBe(
'&lt;img onerror=&quot;alert(&#39;xss&#39;)&quot; &#x2F;&gt;'
);
});
});
describe('escapeHtmlAttribute', () => {
it('escapes double quotes for attribute context', () => {
expect(escapeHtmlAttribute('value"with"quotes')).toContain('&quot;');
});
it('escapes angle brackets', () => {
expect(escapeHtmlAttribute('<tag>')).toContain('&lt;');
});
it('converts non-strings', () => {
expect(escapeHtmlAttribute(42)).toBe('42');
});
});
describe('sanitizeUserInput', () => {
it('returns empty string for null/undefined', () => {
expect(sanitizeUserInput(null)).toBe('');
expect(sanitizeUserInput(undefined)).toBe('');
});
it('trims whitespace by default', () => {
const result = sanitizeUserInput(' hello ');
expect(result).not.toMatch(/^\s/);
expect(result).not.toMatch(/\s$/);
});
it('does not trim when option is false', () => {
const result = sanitizeUserInput(' hello ', { trimWhitespace: false });
// The result is escaped, so check it contains the text
expect(result).toContain('hello');
});
it('enforces maxLength', () => {
const long = 'a'.repeat(200);
const result = sanitizeUserInput(long, { maxLength: 10 });
// After escaping, the core content is truncated to 10 chars
expect(result.length).toBeLessThanOrEqual(10);
});
it('normalizes line breaks', () => {
const input = 'line1\r\nline2\rline3';
const result = sanitizeUserInput(input, { allowLineBreaks: true });
expect(result).not.toContain('\r');
});
it('escapes HTML by default (allowHtml=false)', () => {
const result = sanitizeUserInput('<script>alert(1)</script>');
expect(result).toContain('&lt;');
expect(result).not.toContain('<script>');
});
it('converts non-string input to string', () => {
expect(sanitizeUserInput(42)).toBe('42');
});
});
describe('safeSetInnerHTML', () => {
it('does nothing when element is null', () => {
expect(() => safeSetInnerHTML(null, 'content')).not.toThrow();
});
it('clears content when content is empty/falsy', () => {
const el = document.createElement('div');
el.textContent = 'existing';
safeSetInnerHTML(el, '');
expect(el.textContent).toBe('');
});
it('sets textContent when allowHtmlTags is false', () => {
const el = document.createElement('div');
safeSetInnerHTML(el, '<b>bold</b>', false);
expect(el.textContent).toBe('<b>bold</b>');
expect(el.innerHTML).not.toContain('<b>');
});
it('sets textContent when content is null', () => {
const el = document.createElement('div');
el.textContent = 'existing';
safeSetInnerHTML(el, null);
expect(el.textContent).toBe('');
});
});
describe('safeSetTextContent', () => {
it('does nothing when element is null', () => {
expect(() => safeSetTextContent(null, 'content')).not.toThrow();
});
it('sets text content safely', () => {
const el = document.createElement('div');
safeSetTextContent(el, 'hello world');
expect(el.textContent).toBe('hello world');
});
it('converts content to string', () => {
const el = document.createElement('div');
safeSetTextContent(el, 123);
expect(el.textContent).toBe('123');
});
it('handles null content gracefully', () => {
const el = document.createElement('div');
safeSetTextContent(el, null);
expect(el.textContent).toBe('');
});
it('handles undefined content gracefully', () => {
const el = document.createElement('div');
safeSetTextContent(el, undefined);
expect(el.textContent).toBe('');
});
});
describe('escapeHtml fuzz / property tests', () => {
// The most critical security invariant: after escapeHtml, the result
// should never contain raw <, >, or unescaped quotes that could break
// out of an HTML context. We fuzz with random injection-like strings
// to catch any escaping regression.
const generatePayload = () => {
const fragments = [
'<script>', '</script>', '<img src=x onerror=alert(1)>',
'"><svg/onload=', "'><iframe", '&#x27;', 'javascript:',
'"', "'", '<', '>', '&', '/', '`', '${', 'data:text/html,',
'<style>', '</style>', '</textarea>',
];
// Build a random combination
const len = Math.floor(Math.random() * 5) + 1;
let payload = '';
for (let i = 0; i < len; i++) {
payload += fragments[Math.floor(Math.random() * fragments.length)];
// Insert random characters between
const noise = Math.floor(Math.random() * 5);
for (let j = 0; j < noise; j++) {
payload += String.fromCharCode(Math.floor(Math.random() * 95) + 32);
}
}
return payload;
};
it('escaped output never contains raw < or > (200 random payloads)', () => {
for (let i = 0; i < 200; i++) {
const payload = generatePayload();
const escaped = escapeHtml(payload);
expect(escaped).not.toMatch(/</);
expect(escaped).not.toMatch(/>/);
}
});
it('escaped output never contains raw double quotes', () => {
for (let i = 0; i < 200; i++) {
const payload = generatePayload();
const escaped = escapeHtml(payload);
expect(escaped).not.toMatch(/"/);
}
});
it('escaped output is parseable as text content (round-trips through DOM)', () => {
// If escaping works, the escaped string parsed as innerHTML and read
// back via textContent should equal the original input.
const div = document.createElement('div');
for (let i = 0; i < 100; i++) {
const payload = generatePayload();
const escaped = escapeHtml(payload);
// eslint-disable-next-line no-unsanitized/property -- test code
div.innerHTML = escaped;
// textContent decodes the entities back to the original
expect(div.textContent).toBe(payload);
// And no script/element tags were created
expect(div.children.length).toBe(0);
}
});
it('escaped output never creates executable elements', () => {
const div = document.createElement('div');
// Specific known XSS vectors
const vectors = [
'<script>alert(1)</script>',
'<img src=x onerror=alert(1)>',
'<svg onload=alert(1)>',
'"><script>alert(1)</script>',
'<iframe src=javascript:alert(1)>',
'<a href="javascript:alert(1)">x</a>',
];
for (const vector of vectors) {
div.innerHTML = escapeHtml(vector);
// No element children should have been created
expect(div.children.length).toBe(0);
expect(div.querySelectorAll('script').length).toBe(0);
expect(div.querySelectorAll('img').length).toBe(0);
expect(div.querySelectorAll('svg').length).toBe(0);
expect(div.querySelectorAll('iframe').length).toBe(0);
}
});
it('idempotent: escaping an already-escaped string does not create XSS', () => {
// Escaping twice should still be safe (no element creation)
const div = document.createElement('div');
for (let i = 0; i < 50; i++) {
const payload = generatePayload();
const escapedTwice = escapeHtml(escapeHtml(payload));
// eslint-disable-next-line no-unsanitized/property -- test code
div.innerHTML = escapedTwice;
expect(div.children.length).toBe(0);
}
});
});