diff --git a/src/__tests__/utils/output.test.ts b/src/__tests__/utils/output.test.ts index 97c4deb..49b89c1 100644 --- a/src/__tests__/utils/output.test.ts +++ b/src/__tests__/utils/output.test.ts @@ -153,4 +153,41 @@ describe('utils/output.print writes correct format from extension', ()=>{ const content = fs.readFileSync(out, 'utf8'); expect(JSON.parse(content)).toEqual([{a: 1}]); }); + it('-o file preserves terminal escape sequences', ()=>{ + const out = make_tmp('.txt'); + const content = 'hello\x1b[31mRED\x1b[0m'; + print(content, {output: out}); + expect(fs.readFileSync(out, 'utf8')).toBe(content); + }); +}); + +describe('utils/output.print terminal sanitization', ()=>{ + let stdout_write: ReturnType; + beforeEach(()=>{ + stdout_write = vi.spyOn(process.stdout, 'write') + .mockImplementation(()=>true); + }); + afterEach(()=>{ + vi.restoreAllMocks(); + }); + it('strips terminal escape sequences before writing to stdout', ()=>{ + const malicious = 'hello' + + '\x1b[2J' + + '\x1b[31mRED\x1b[0m' + + '\x1b]0;Title-pwn\x07' + + 'world'; + print(malicious); + const output = stdout_write.mock.calls + .map((call: unknown[])=>String(call[0])) + .join(''); + expect(output).toBe('helloREDworld\n'); + expect(output).not.toContain('\x1b'); + }); + it('keeps normal stdout content unchanged', ()=>{ + print('hello world'); + const output = stdout_write.mock.calls + .map((call: unknown[])=>String(call[0])) + .join(''); + expect(output).toBe('hello world\n'); + }); }); diff --git a/src/utils/output.ts b/src/utils/output.ts index 7869d02..5cff6ca 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -1,5 +1,9 @@ import fs from 'fs'; import path from 'path'; +import { stripVTControlCharacters } from 'util'; + +const terminal_safe = (val: unknown): string=> + stripVTControlCharacters(String(val)); const is_tty = process.stdout.isTTY === true; @@ -183,7 +187,8 @@ const print = (data: unknown, opts: Print_opts = {})=>{ } if (!is_tty && fmt == 'raw') fmt = typeof data == 'string' ? 'raw' : 'json'; - process.stdout.write(serialize(data, fmt)+'\n'); + const content = serialize(data, fmt); + process.stdout.write(terminal_safe(content) + '\n'); }; const print_table = (rows: Record[], cols: string[])=>{