Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node for renderer security tests
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Test shipped Markdown renderer
run: node --test tests/test_web_renderer.cjs

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_web_renderer.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const { test } = require('node:test');

// Exercise the shipped renderer without starting the UI or evaluating its HTML.
const source = fs.readFileSync(path.join(__dirname, '../web_companion/app.js'), 'utf8');
const start = source.indexOf(' function escapeHtml(');
const end = source.indexOf(' function renderPreview()', start);
assert.ok(start >= 0 && end > start);
const renderer = { t: () => '' };
vm.createContext(renderer);
vm.runInContext(source.slice(start, end), renderer);

test('ordinary fenced code preserves language and escapes body', () => {
assert.equal(renderer.renderMarkdown('```python\n<x> & "quoted"\n```'),
'<pre><code class="language-python">&lt;x&gt; &amp; &quot;quoted&quot;</code></pre>');
});

for (const language of ['"><img src=x onerror=probe>', '" onmouseover="probe',
"'<> &", '&quot;><img src=x>', '日本語']) {
test(`fence language remains one escaped attribute: ${JSON.stringify(language)}`, () => {
const output = renderer.renderMarkdown('```' + language + '\nbody\n```');
assert.equal(output, '<pre><code class="language-' + renderer.escapeHtml(language)
+ '">body</code></pre>');
assert.deepEqual(output.match(/<[^>]+>/g), ['<pre>',
'<code class="language-' + renderer.escapeHtml(language) + '">', '</code>', '</pre>']);
});
}
2 changes: 1 addition & 1 deletion web_companion/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@
} else {
inCodeBlock = false;
const codeContent = escapeHtml(codeBlockLines.join('\n'));
out.push(`<pre><code class="language-${codeBlockLang}">${codeContent}</code></pre>`);
out.push(`<pre><code class="language-${escapeHtml(codeBlockLang)}">${codeContent}</code></pre>`);
}
continue;
}
Expand Down
Loading