










@@ -494,6 +494,104 @@ describe("export html security hardening", () => {
494494);
495495});
496496497+it("escapes entry.id in element id and data-entry-id attributes", async () => {
498+const xssId = `"><script>alert(1)</script><div data-x="`;
499+const session: SessionData = {
500+header: { id: "session-xss-id", timestamp: now() },
501+entries: [
502+{
503+id: xssId,
504+parentId: null,
505+timestamp: now(),
506+type: "message",
507+message: { role: "user", content: "hello" },
508+},
509+{
510+id: "safe-child",
511+parentId: xssId,
512+timestamp: now(),
513+type: "message",
514+message: {
515+role: "assistant",
516+content: [{ type: "text", text: "world" }],
517+},
518+},
519+],
520+leafId: "safe-child",
521+systemPrompt: "",
522+tools: [],
523+};
524+525+const { document } = await renderTemplate(session);
526+const messages = requireElement(document.getElementById("messages"), "messages root missing");
527+528+// Core XSS prevention: no <script> tags should be injected into the DOM
529+expect(messages.querySelectorAll("script").length).toBe(0);
530+531+// No attribute breakout: onmouseover, data-x must not appear as real attributes
532+expect(messages.querySelector("[onmouseover]")).toBeNull();
533+534+// The copy-link button must exist with the payload safely contained
535+const copyBtn = requireElement(
536+messages.querySelector(".copy-link-btn"),
537+"copy-link button missing",
538+);
539+// data-entry-id must be present as a proper attribute (not broken out)
540+expect(copyBtn.hasAttribute("data-entry-id")).toBe(true);
541+// No stray attributes from the payload
542+expect(copyBtn.hasAttribute("data-x")).toBe(false);
543+544+// The user message element must not have attribute breakout either
545+const userMsg = requireElement(
546+messages.querySelector(".user-message"),
547+"user message element missing",
548+);
549+expect(userMsg.getAttribute("data-x")).toBeNull();
550+// The element id must start with entry- (the payload is contained within)
551+const elementId = userMsg.getAttribute("id") ?? "";
552+expect(elementId.startsWith("entry-")).toBe(true);
553+});
554+555+it("copy-link round-trip: dataset.entryId matches raw entry.id after browser decoding", async () => {
556+// IDs with characters that need HTML escaping but should round-trip correctly
557+const specialId = `msg-with"quotes&'s`;
558+const session: SessionData = {
559+header: { id: "session-roundtrip", timestamp: now() },
560+entries: [
561+{
562+id: specialId,
563+parentId: null,
564+timestamp: now(),
565+type: "message",
566+message: { role: "user", content: "test" },
567+},
568+],
569+leafId: specialId,
570+systemPrompt: "",
571+tools: [],
572+};
573+574+const { document } = await renderTemplate(session);
575+const messages = requireElement(document.getElementById("messages"), "messages root missing");
576+577+// The copy-link button should exist
578+const copyBtn = requireElement(
579+messages.querySelector(".copy-link-btn"),
580+"copy-link button missing",
581+);
582+583+// Browser decodes HTML entities in dataset reads, so dataset.entryId
584+// must return the RAW entry.id (not the HTML-escaped version).
585+// This is essential for buildShareUrl() to produce the correct URL.
586+const datasetValue = (copyBtn as HTMLElement).dataset.entryId;
587+expect(datasetValue).toBe(specialId);
588+589+// The DOM element id must also round-trip: getElementById should find it
590+const userMsg = document.getElementById(`entry-${specialId}`);
591+expect(userMsg).not.toBeNull();
592+expect(userMsg?.classList.contains("user-message")).toBe(true);
593+});
594+497595it("escapes markdown data-image attributes", async () => {
498596const dataImage = "data:image/png;base64,AAAA";
499597const session: SessionData = {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。