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');
+});