Skip to content
Open
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
7 changes: 5 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@uiw/codemirror-theme-xcode": "^4.21.21",
"@uiw/react-markdown-preview": "^5.0.7",
"@vscode/markdown-it-katex": "^1.1.0",
"@yorkie-js/sdk": "0.6.28",
"axios": "^1.6.5",
"browser-image-resizer": "^2.4.1",
"clipboardy": "^4.0.0",
Expand All @@ -50,7 +51,9 @@
"color": "^4.2.3",
"form-data": "^4.0.0",
"hast-util-to-html": "^9.0.3",
"html2canvas": "^1.4.1",
"incremental-dom": "^0.6.0",
"jspdf": "^3.0.3",
"katex": "^0.16.9",
"lib0": "^0.2.88",
"lodash": "^4.17.21",
Expand All @@ -61,6 +64,7 @@
"markdown-it-prism": "^2.3.0",
"markdown-it-sanitizer": "^0.4.3",
"markdown-it-task-checkbox": "^1.0.6",
"marked": "^16.3.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add HTML sanitization dependency (DOMPurify) to mitigate XSS.

You render marked output via innerHTML in the PDF path. Marked does not sanitize by default; sanitize before injecting.

Apply this to dependencies (pick latest stable):

   "dependencies": {
+    "dompurify": "^3.0.0",
     "html2canvas": "^1.4.1",
     "jspdf": "^3.0.3",
     "marked": "^16.3.0",

Please confirm the latest safe DOMPurify version and whether a caret range is acceptable in this repo.


🏁 Script executed:

#!/bin/bash
# Show dependencies block to verify version syntax
sed -n '1,200p' frontend/package.json

# Search for innerHTML usage in JS/TS files
rg -n 'innerHTML' -g '*.js' -g '*.ts'

# Search for marked() invocation in JS/TS files
rg -n 'marked\(' -g '*.js' -g '*.ts'

Length of output: 3604


🌐 Web query:

What is the latest stable version of DOMPurify?

💡 Result:

The latest stable DOMPurify release is 3.2.7 (released Sep 17, 2025). [1][2]

Sources:
[1] GitHub Releases — cure53/DOMPurify (3.2.7).
[2] Snyk / npm package info for dompurify (latest: 3.2.7).


Add DOMPurify v3.2.7 dependency for sanitizing marked output. You render marked output via innerHTML in frontend/src/hooks/useFileExport.ts; sanitize before injecting. Add to dependencies:

 "dependencies": {
+  "dompurify": "^3.2.7",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"marked": "^16.3.0",
"dependencies": {
"dompurify": "^3.2.7",
"marked": "^16.3.0",
🤖 Prompt for AI Agents
In frontend/package.json around line 67, add the dependency "dompurify": "3.2.7"
to dependencies and update the code that injects marked HTML
(frontend/src/hooks/useFileExport.ts) to sanitize before using innerHTML;
specifically install/add the dependency to package.json, run yarn/npm install,
import DOMPurify in useFileExport (or access window.DOMPurify) and replace
direct innerHTML assignment with innerHTML = DOMPurify.sanitize(marked(...));
ensure TypeScript imports/types are handled (install @types/dompurify if needed)
and update tests/build accordingly.

"match-sorter": "^6.3.3",
"moment": "^2.30.1",
"notistack": "^3.0.1",
Expand All @@ -81,8 +85,7 @@
"redux-persist": "^6.0.0",
"refractor": "^4.8.1",
"validator": "^13.12.0",
"vite-plugin-package-version": "^1.1.0",
"@yorkie-js/sdk": "0.6.28"
"vite-plugin-package-version": "^1.1.0"
},
"devDependencies": {
"@sentry/vite-plugin": "^2.20.1",
Expand Down
91 changes: 78 additions & 13 deletions frontend/src/hooks/useFileExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useCallback } from "react";
import { useSelector } from "react-redux";
import { selectDocument } from "../store/documentSlice";
import { selectEditor } from "../store/editorSlice";
import { useExportFileMutation } from "./api/file";
import { marked } from "marked";
import { jsPDF } from "jspdf";
import html2canvas from "html2canvas";

export const enum FileExtension {
Markdown = "markdown",
Expand All @@ -24,28 +26,91 @@ export const useFileExport = (): UseFileExportReturn => {
const editorStore = useSelector(selectEditor);
const documentStore = useSelector(selectDocument);

const exportFileMutation = useExportFileMutation();

const handleExportFile = useCallback(
async (exportType: string) => {
try {
Comment on lines 29 to 31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Tighten types: use FileExtension instead of string.

Prevents unsupported values and improves autocomplete.

-  async (exportType: string) => {
+  async (exportType: FileExtension) => {

Also applies to: 42-113

🤖 Prompt for AI Agents
In frontend/src/hooks/useFileExport.ts around lines 29-31 (and also apply
changes through lines 42-113), the handleExportFile parameter is typed as a
plain string; change its type to the FileExtension union/enum used across the
codebase. Import or reference the existing FileExtension type at the top of the
file, update the handleExportFile signature to use FileExtension instead of
string, and update any local variables, calls, and switch/conditional logic in
lines 42-113 to expect FileExtension (adjust casts or validations where
necessary). Ensure any external callers pass a FileExtension value or
map/validate inputs to FileExtension before calling.

const markdown = editorStore.doc?.getRoot().content?.toString() || "";
const documentName = documentStore.data?.title || "codepair_document";

enqueueSnackbar(`${exportType.toUpperCase()} file export started`, {
variant: "info",
});

const response = await exportFileMutation.mutateAsync({
exportType,
content: markdown,
fileName: documentName,
});
let blob: Blob;
let fileName: string;

switch (exportType) {
case FileExtension.Markdown:
blob = new Blob([markdown], { type: "text/markdown" });
fileName = `${documentName}.md`;
break;
case FileExtension.HTML:
{
const html = await marked(markdown);
blob = new Blob([html], { type: "text/html" });
fileName = `${documentName}.html`;
}
break;
Comment on lines +47 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Sanitize HTML before innerHTML to prevent XSS.

marked output is unsanitized; assigning to innerHTML is exploitable even off‑screen. Sanitize for both HTML and PDF paths.

+import DOMPurify from "dompurify";
...
-          {
-            const html = await marked(markdown);
-            blob = new Blob([html], { type: "text/html" });
+          {
+            const { marked } = await import("marked");
+            const rawHtml = await marked(markdown);
+            const safeHtml = DOMPurify.sanitize(rawHtml);
+            blob = new Blob([safeHtml], { type: "text/html;charset=utf-8" });
             fileName = `${documentName}.html`;
           }
...
-          {
-            const html = await marked(markdown);
+          {
+            const [{ default: html2canvas }, { jsPDF }, { marked }] = await Promise.all([
+              import("html2canvas"),
+              import("jspdf"),
+              import("marked"),
+            ]);
+            const rawHtml = await marked(markdown);
+            const safeHtml = DOMPurify.sanitize(rawHtml);
             const element = document.createElement("div");
-            element.innerHTML = html;
+            element.innerHTML = safeHtml;

Based on static analysis hints.

Also applies to: 56-66, 58-71

🤖 Prompt for AI Agents
In frontend/src/hooks/useFileExport.ts around lines 47 to 53 (and similarly
56-66, 58-71), the HTML generated by marked is unsanitized and later assigned to
innerHTML (and used in the PDF path), leaving an XSS risk; update the code to
sanitize marked(markdown) before creating the Blob or injecting into the DOM:
import a trusted sanitizer (e.g., DOMPurify), run const cleanHtml =
DOMPurify.sanitize(html, {ALLOWED_TAGS: ...} or appropriate config), then use
cleanHtml when creating the Blob for HTML export and when passing HTML into the
PDF rendering flow; ensure the sanitizer is added to package.json, typed
correctly (or use any available DOMPurify types), and include a short comment
noting why sanitization is required.

case FileExtension.PDF:
{
const html = await marked(markdown);

const element = document.createElement("div");
element.innerHTML = html;
element.style.width = "794px"; // A4
element.style.padding = "40px";
element.style.fontFamily = "Arial, sans-serif";
element.style.fontSize = "14px";
element.style.lineHeight = "1.6";
element.style.color = "#000000";
element.style.backgroundColor = "#ffffff";
element.style.position = "absolute";
element.style.left = "-9999px";
element.style.top = "0";

document.body.appendChild(element);

try {
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: "#ffffff",
});

const imgData = canvas.toDataURL("image/png");
const pdf = new jsPDF({
orientation: "portrait",
unit: "mm",
format: "a4",
});

const imgWidth = 210;
const pageHeight = 297; // A4
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;

pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;

const contentDisposition = response.headers["content-disposition"];
const fileNameMatch = contentDisposition?.match(/filename="?(.+)"?\s*$/i);
const fileName = fileNameMatch ? fileNameMatch[1] : `${documentName}.${exportType}`;
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}

const blob = new Blob([response.data], { type: response.headers["content-type"] });
blob = pdf.output("blob");
fileName = `${documentName}.pdf`;
} finally {
document.body.removeChild(element);
}
}
break;
default:
throw new Error(`Unsupported export type: ${exportType}`);
}

const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
Expand All @@ -66,7 +131,7 @@ export const useFileExport = (): UseFileExportReturn => {
});
}
},
[editorStore, documentStore, enqueueSnackbar, exportFileMutation]
[editorStore, documentStore, enqueueSnackbar]
);

const handleExportToPDF = () => handleExportFile(FileExtension.PDF);
Expand Down
Loading