diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ae440c4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,15 @@ +name: CI + +on: + push: + branches: [main, 'cursor/**'] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests + run: node --test tests/*.mjs diff --git a/index.html b/index.html index df4b1f7..c5831d3 100644 --- a/index.html +++ b/index.html @@ -204,6 +204,15 @@

免费在线工具箱

navigator.clipboard.writeText(text).then(() => toast('已复制到剪贴板')); } +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + // ============ 生成工具面板 ============ function renderPanels() { const container = document.getElementById('toolPanels'); @@ -626,10 +635,10 @@

🔍 正则测试 ← 返回< let html = ''; for (let i = 0; i < maxLen; i++) { const la = a[i] ?? '', lb = b[i] ?? ''; - if (la === lb) { html += `
${la}
`; } + if (la === lb) { html += `
${escapeHtml(la)}
`; } else { - if (la) html += `
- ${la}
`; - if (lb) html += `
+ ${lb}
`; + if (la) html += `
- ${escapeHtml(la)}
`; + if (lb) html += `
+ ${escapeHtml(lb)}
`; } } document.getElementById('diff-result').innerHTML = html; @@ -668,7 +677,7 @@

🔍 正则测试 ← 返回< // Markdown (简单实现) function renderMD() { const md = document.getElementById('md-input').value; - let html = md + let html = escapeHtml(md) .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') @@ -720,12 +729,12 @@

🔍 正则测试 ← 返回< const re = new RegExp(pattern, flags); const matches = text.match(re); if (matches) { - document.getElementById('regex-result').innerHTML = `匹配 ${matches.length} 个:
` + matches.map(m => `${m}`).join(' '); + document.getElementById('regex-result').innerHTML = `匹配 ${matches.length} 个:
` + matches.map(m => `${escapeHtml(m)}`).join(' '); } else { document.getElementById('regex-result').innerHTML = '无匹配'; } } catch(e) { - document.getElementById('regex-result').innerHTML = `❌ ${e.message}`; + document.getElementById('regex-result').innerHTML = `❌ ${escapeHtml(e.message)}`; } } diff --git a/tests/escape-html.test.mjs b/tests/escape-html.test.mjs new file mode 100644 index 0000000..ea93f6a --- /dev/null +++ b/tests/escape-html.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +/** + * 与 index.html 中 escapeHtml 实现保持一致(用于锁定 XSS 缓解逻辑)。 + */ +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +test('escapeHtml 转义尖括号与引号', () => { + assert.equal( + escapeHtml(''), + '<img src=x onerror=alert(1)>' + ); + assert.equal(escapeHtml('a"b\'c'), 'a"b'c'); +}); + +test('escapeHtml 先处理 & 避免双重实体被误解析', () => { + assert.equal(escapeHtml('Tom & Jerry'), 'Tom & Jerry'); +});