import { describe, expect, it } from 'vitest'; /** * Security tests for injection prevention * These tests verify that the fixes for the RCE vulnerability are effective */ describe('Injection Prevention', () => { describe('JSON.stringify escaping', () => { it('should wrap input in quotes, making backticks safe', () => { const maliciousInput = '`+alert(1)+`'; const escaped = JSON.stringify(maliciousInput); // JSON.stringify returns a quoted string: "`+alert(1)+`" // When used in code like: let x = ${escaped}; // It becomes: let x = "`+alert(1)+`"; // The backticks are inside quotes, so they're treated as regular characters expect(escaped).toBe('"`+alert(1)+`"'); expect(escaped.startsWith('"')).toBe(true); expect(escaped.endsWith('"')).toBe(true); }); it('should escape template literal syntax safely', () => { const maliciousInput = '${process.binding("spawn_sync")}'; const escaped = JSON.stringify(maliciousInput); expect(escaped).toBe('"${process.binding(\\"spawn_sync\\")}"'); // Verify it's wrapped in quotes and inner quotes are escaped expect(escaped.startsWith('"')).toBe(true); expect(escaped.endsWith('"')).toBe(true); }); it('should handle newlines safely', () => { const maliciousInput = '`\nalert(1)\n`'; const escaped = JSON.stringify(maliciousInput); // Newlines are escaped as \n in the JSON string expect(escaped).toContain('\\n'); expect(escaped).toBe('"`\\nalert(1)\\n`"'); }); it('should escape quotes', () => { const maliciousInput = '"; alert(1); "'; const escaped = JSON.stringify(maliciousInput); // JSON.stringify escapes the quotes expect(escaped).toContain('\\"'); expect(escaped.startsWith('"')).toBe(true); expect(escaped.endsWith('"')).toBe(true); }); it('should preserve CJK characters', () => { const cjkInput = '新条目'; const escaped = JSON.stringify(cjkInput); expect(escaped).toBe('"新条目"'); }); it('should preserve Unicode characters', () => { const unicodeInput = 'Tëst Ñame 日本語 한글'; const escaped = JSON.stringify(unicodeInput); expect(escaped).toContain('Tëst'); expect(escaped).toContain('日本語'); expect(escaped).toContain('한글'); }); }); describe('Script generation safety', () => { it('should prevent template literal breakout in openTiddler', () => { const maliciousName = '`+void window.service.wiki.wikiOperationInServer()+`'; // Simulate the fixed version const script = ` let trimmedTiddlerName = ${JSON.stringify(maliciousName)}; let currentHandlerWidget = $tw.rootWidget; `; // The malicious code should be trapped inside a quoted string // It becomes: let trimmedTiddlerName = "`+void...+`"; // The backticks are inside quotes, so they're just characters expect(script).toContain('"`+void window.service'); expect(script).not.toContain('void window.service.wiki.wikiOperationInServer()+`;\n'); }); it('should prevent injection in setTiddlerText', () => { const maliciousTitle = 'dummy`),process.binding("spawn_sync"),console.log(1);//'; const maliciousValue = 'New content'; // Simulate the fixed version const script = `return $tw.wiki.setText(${JSON.stringify(maliciousTitle)}, 'text', undefined, ${JSON.stringify(maliciousValue)});`; // The script should have quoted strings, not executable code expect(script).toContain('"dummy`),process.binding'); expect(script).toContain('"New content"'); }); it('should prevent injection in renderWikiText', () => { const maliciousContent = '`+alert(1)+`'; // Simulate the fixed version const script = `return $tw.wiki.renderText("text/html", "text/vnd.tiddlywiki", ${JSON.stringify(maliciousContent)});`; // alert(1) should be inside a quoted string expect(script).toContain('"`+alert(1)+`"'); }); }); describe('Real-world attack vectors', () => { it('should block the reported PoC payload', () => { const pocPayload = "dummy`),process.binding('spawn_sync').spawn({file:'/usr/bin/open',args:['/usr/bin/open','/System/Applications/Calculator.app/Contents/MacOS/Calculator'],stdio:[{type:'pipe',readable:!0},{type:'pipe',writable:!0},{type:'pipe',writable:!0}]}),console.log(1);//"; const escaped = JSON.stringify(pocPayload); // Should be wrapped in quotes, making the backticks safe expect(escaped.startsWith('"')).toBe(true); expect(escaped.endsWith('"')).toBe(true); // The payload is now a string literal, not executable code expect(escaped).toContain('"dummy`),process.binding'); }); it('should block XSS in renderer via openTiddler', () => { const xssPayload = "`+alert('XSS')+`"; const script = `let trimmedTiddlerName = ${JSON.stringify(xssPayload)};`; // alert('XSS') should be inside a quoted string expect(script).toContain('"`+alert(\'XSS\')+`"'); }); it('should block various injection techniques', () => { const payloads = [ '`+alert(1)+`', '${alert(1)}', '\\`+alert(1)+\\`', "'; alert(1); '", '"); alert(1); ("', '`; alert(1); `', '`\\nalert(1)\\n`', ]; for (const payload of payloads) { const escaped = JSON.stringify(payload); // All should be wrapped in quotes, making them safe string literals expect(escaped.startsWith('"')).toBe(true); expect(escaped.endsWith('"')).toBe(true); } }); }); describe('Deep link sanitization', () => { /** * Simulates the sanitizeTiddlerName method from DeepLinkService */ function sanitizeTiddlerName(tiddlerName: string): string { let sanitized = tiddlerName; // Remove backticks and template literal syntax sanitized = sanitized.replace(/[`${}]/g, ''); // Remove HTML tags and script tags sanitized = sanitized.replace(/<\/?[^>]+(>|$)/g, ''); // Remove newlines and other control characters sanitized = sanitized.replace(/[\r\n\t]/g, ' '); // Remove null bytes sanitized = sanitized.replace(/\0/g, ''); // Trim whitespace sanitized = sanitized.trim(); // Limit length to prevent DoS if (sanitized.length > 1000) { sanitized = sanitized.substring(0, 1000); } return sanitized; } it('should remove backticks', () => { const input = '`malicious`'; const sanitized = sanitizeTiddlerName(input); expect(sanitized).toBe('malicious'); expect(sanitized).not.toContain('`'); }); it('should remove template literal syntax', () => { const input = '${process.binding()}'; const sanitized = sanitizeTiddlerName(input); expect(sanitized).toBe('process.binding()'); expect(sanitized).not.toContain('$'); expect(sanitized).not.toContain('{'); expect(sanitized).not.toContain('}'); }); it('should remove HTML tags', () => { const input = ''; const sanitized = sanitizeTiddlerName(input); expect(sanitized).toBe('alert(1)'); expect(sanitized).not.toContain('