refactor: avoid xss attack

This commit is contained in:
Jin 2026-03-09 22:31:49 +00:00 committed by JYC333
parent df60e34072
commit d504946de0
2 changed files with 36 additions and 17 deletions

View File

@ -180,7 +180,7 @@
---
### Step 6: 迁移"关闭弹窗"逻辑 + `attribute_detail.ts` 引用
### Step 6: 迁移"关闭弹窗"逻辑 + `attribute_detail.ts` 引用 ✅ 完成
**文件变更:**
- `apps/client/src/services/dialog.ts` — 替换 `$(".aa-input").autocomplete("close")`
- `apps/client/src/components/entrypoints.ts` — 替换 `$(".aa-input").autocomplete("close")`
@ -188,12 +188,12 @@
- `apps/client/src/widgets/attribute_widgets/attribute_detail.ts` — 更新 `.algolia-autocomplete` 选择器
**说明:**
需要一个全局的"关闭所有 autocomplete"机制。方案:维护一个全局 `Set<AutocompleteApi>`,在各处调用时遍历关闭。可以放在 `note_autocomplete.ts` 中导出
引入了全局的 "关闭所有 headless autocomplete" 机制(通过 `closeAllHeadlessAutocompletes` 方法)
**验证方式:**
- autocomplete 弹窗打开时切换标签页 → 弹窗自动关闭
- autocomplete 弹窗打开时打开对话框 → 弹窗自动关闭
- 点击 autocomplete 下拉菜单时属性面板不应关闭
- autocomplete 弹窗打开时切换标签页 → 弹窗自动关闭
- autocomplete 弹窗打开时打开对话框 → 弹窗自动关闭
- 点击 autocomplete 下拉菜单时属性面板不应关闭
---

View File

@ -82,14 +82,33 @@ function escapeHtml(text: string): string {
}
function sanitizeHighlightedHtml(text: string, { allowBreaks = false }: { allowBreaks?: boolean } = {}): string {
const sanitizedBreaks = allowBreaks
? text.replace(/<br\b[^>]*\/?>/gi, "<br>")
: text.replace(/<br\b[^>]*\/?>/gi, "");
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const safeOutput = document.createDocumentFragment();
return sanitizedBreaks
.replace(/<b\b[^>]*>/gi, "<b>")
.replace(/<\/b\s*>/gi, "</b>")
.replace(/<\/?[^>]+>/g, "");
const processNode = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
safeOutput.appendChild(document.createTextNode(node.textContent || ""));
} else if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
if (el.tagName === "B") {
const b = document.createElement("b");
b.textContent = el.textContent; // Only extract text, stripping nested potential danger
safeOutput.appendChild(b);
} else if (el.tagName === "BR" && allowBreaks) {
safeOutput.appendChild(document.createElement("br"));
} else {
// If the tag is not allowed, just extract its text content securely
safeOutput.appendChild(document.createTextNode(el.textContent || ""));
}
}
};
doc.body.childNodes.forEach(processNode);
const tmpDiv = document.createElement("div");
tmpDiv.appendChild(safeOutput);
return tmpDiv.innerHTML;
}
function normalizeAttributeSnippet(snippet: string): string {
@ -302,7 +321,7 @@ async function fetchResolvedSuggestions(term: string, options: Options = {}): Pr
return commandSuggestions;
}
const fastSearch = options.fastSearch === false ? false : true;
const fastSearch = options.fastSearch !== false;
if (fastSearch === false) {
if (term.trim().length === 0) {
return [];
@ -804,9 +823,9 @@ function init() {
$.fn.getSelectedNotePath = function () {
if (!String($(this).val())?.trim()) {
return "";
} else {
return $(this).attr(SELECTED_NOTE_PATH_KEY);
}
return $(this).attr(SELECTED_NOTE_PATH_KEY);
};
$.fn.getSelectedNoteId = function () {
@ -830,9 +849,9 @@ function init() {
$.fn.getSelectedExternalLink = function () {
if (!String($(this).val())?.trim()) {
return "";
} else {
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
}
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
};
$.fn.setSelectedExternalLink = function (externalLink: string | null) {