|
| 1 | +/** |
| 2 | + * Part A — Golden-parity: JS renderer path for the adversarial bash tool_result. |
| 3 | + * |
| 4 | + * The Python md-exporter path is covered by |
| 5 | + * ``tests/test_dual_path_parity_oracle.py``. Both suites use the same |
| 6 | + * adversarial payload (``<img src=x onerror=alert(1)>``). |
| 7 | + * |
| 8 | + * These tests drive the **real** JS dispatch path via ``renderToolResult`` |
| 9 | + * (registry.js) and the direct renderer ``renderBashResult`` (bash.js) — no |
| 10 | + * reimplementations. |
| 11 | + * |
| 12 | + * Oracle discipline (test-review C3/C8/C9): |
| 13 | + * Every assertion checks the real observable HTML output, never merely that |
| 14 | + * the function does not throw. |
| 15 | + */ |
| 16 | + |
| 17 | +import { describe, it, expect } from 'vitest'; |
| 18 | +import { renderToolResult } from '../registry.js'; |
| 19 | +import { renderBashResult } from './bash.js'; |
| 20 | + |
| 21 | +// Shared adversarial payload — must match the Python constant in |
| 22 | +// tests/test_dual_path_parity_oracle.py. |
| 23 | +const PAYLOAD = '<img src=x onerror=alert(1)>'; |
| 24 | + |
| 25 | +// Parsed bash dict that mirrors what ``_parse_tool_result`` / ``_tool_result_build_bash`` |
| 26 | +// would produce for the adversarial blob. |
| 27 | +const ADVERSARIAL_PARSED = { |
| 28 | + result_type: 'bash', |
| 29 | + slug: null, |
| 30 | + stdout: PAYLOAD, |
| 31 | + stderr: '', |
| 32 | + exit_code: 0, |
| 33 | + interrupted: false, |
| 34 | + is_error: false, |
| 35 | + return_code_interpretation: 'success', |
| 36 | +}; |
| 37 | + |
| 38 | +describe('Part A: golden-parity — adversarial bash result (JS renderer path)', () => { |
| 39 | + it('renders a non-empty HTML string for the adversarial payload', () => { |
| 40 | + // Oracle: output must not be empty — the payload is not silently dropped. |
| 41 | + const html = renderToolResult(ADVERSARIAL_PARSED); |
| 42 | + expect(typeof html).toBe('string'); |
| 43 | + expect(html.length).toBeGreaterThan(0); |
| 44 | + }); |
| 45 | + |
| 46 | + it('HTML-escapes the adversarial payload — raw XSS tag must not appear', () => { |
| 47 | + // Oracle: the raw ``<img src=x onerror=`` string must be absent (escaped); |
| 48 | + // the HTML-entity form must be present. |
| 49 | + // |
| 50 | + // Negative control: if esc() were removed from renderBashResult, the raw |
| 51 | + // tag WOULD appear in the output and ``not.toContain`` would fail — |
| 52 | + // a regressed renderer cannot pass this test. |
| 53 | + const html = renderToolResult(ADVERSARIAL_PARSED); |
| 54 | + expect(html).not.toContain('<img src=x onerror='); |
| 55 | + expect(html).toContain('<img src=x onerror=alert(1)>'); |
| 56 | + }); |
| 57 | + |
| 58 | + it('routes through the real renderBashResult dispatch path — no re-implementation', () => { |
| 59 | + // Driving both the registry dispatch (renderToolResult) and the direct |
| 60 | + // renderer (renderBashResult) confirms they produce the same output. |
| 61 | + // If registry.js ever wired a different renderer for result_type 'bash', |
| 62 | + // this equality assertion would fail. |
| 63 | + const viaRegistry = renderToolResult(ADVERSARIAL_PARSED); |
| 64 | + const viaDirect = renderBashResult(ADVERSARIAL_PARSED); |
| 65 | + expect(viaRegistry).toBe(viaDirect); |
| 66 | + }); |
| 67 | + |
| 68 | + it('negative control: the raw payload is a dangerous string without escaping', () => { |
| 69 | + // Documents why esc() is load-bearing: PAYLOAD itself contains the |
| 70 | + // executable XSS vector. If the renderer passed it through unchanged, |
| 71 | + // the browser could execute it. |
| 72 | + expect(PAYLOAD).toContain('<img'); |
| 73 | + expect(PAYLOAD).toContain('onerror='); |
| 74 | + expect(PAYLOAD).not.toContain('<'); |
| 75 | + |
| 76 | + // The renderer must NOT pass PAYLOAD through unchanged. |
| 77 | + const html = renderBashResult(ADVERSARIAL_PARSED); |
| 78 | + expect(html).not.toContain(PAYLOAD); |
| 79 | + expect(html).toContain('<'); |
| 80 | + }); |
| 81 | +}); |
0 commit comments