Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e034fabe62 |
@@ -1,4 +0,0 @@
|
|||||||
## 2023-10-27 - Excalidraw Component Re-render Performance Penalty
|
|
||||||
|
|
||||||
**Learning:** Due to the exceptionally high rendering cost of the `@excalidraw/excalidraw` canvas, inline functions passed as props (like `onChange` and `onExcalidrawAPI`) cause severe performance degradation by defeating `React.memo` and triggering full canvas re-renders when parent state changes.
|
|
||||||
**Action:** Always strictly memoize all callbacks passed to the Excalidraw component using `useCallback` to preserve prop stability and prevent unnecessary, expensive re-renders.
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
## 2024-06-04 - Defense in Depth: Shiki HTML Sanitization
|
||||||
|
**Vulnerability:** Potential XSS via `dangerouslySetInnerHTML` in CodeBlock components rendering Shiki highlighted code.
|
||||||
|
**Learning:** While Shiki is generally considered safe as it just wraps code in spans, passing user-controlled input (even if it's "code") through an external library and directly into `dangerouslySetInnerHTML` without explicit sanitization creates an unnecessary risk surface. A vulnerability or misconfiguration in the highlighter could lead to XSS.
|
||||||
|
**Prevention:** Always wrap the output of HTML generators (like Shiki) in a dedicated sanitizer (like DOMPurify) before using `dangerouslySetInnerHTML`, even if the generator is deemed "safe." This is a core tenet of defense in depth.
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "excali",
|
"name": "excali",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/excalidraw": "^0.18.1",
|
"@excalidraw/excalidraw": "^0.18.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"dompurify": "^3.4.8",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"mcp-handler": "^1.1.0",
|
"mcp-handler": "^1.1.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.15",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
@@ -217,6 +218,8 @@
|
|||||||
|
|
||||||
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
||||||
|
|
||||||
|
"@types/dompurify": ["@types/dompurify@3.2.0", "", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="],
|
||||||
|
|
||||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||||
|
|
||||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
@@ -393,7 +396,7 @@
|
|||||||
|
|
||||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
"dompurify": ["dompurify@3.4.5", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA=="],
|
"dompurify": ["dompurify@3.4.8", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ=="],
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
@@ -843,6 +846,8 @@
|
|||||||
|
|
||||||
"mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
|
"mermaid/@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
|
||||||
|
|
||||||
|
"mermaid/dompurify": ["dompurify@3.4.5", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA=="],
|
||||||
|
|
||||||
"mermaid/roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
"mermaid/roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
||||||
|
|
||||||
"points-on-path/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
"points-on-path/points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/excalidraw": "^0.18.1",
|
"@excalidraw/excalidraw": "^0.18.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"dompurify": "^3.4.8",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"mcp-handler": "^1.1.0",
|
"mcp-handler": "^1.1.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.15",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
|
|||||||
+3
-5
@@ -307,10 +307,6 @@ function PrivateApp() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleExcalidrawAPI = useCallback((api: ExcalidrawImperativeAPI) => {
|
|
||||||
excalidrawApiRef.current = api;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell" ref={appShellRef}>
|
<div className="app-shell" ref={appShellRef}>
|
||||||
{toastMessage && (
|
{toastMessage && (
|
||||||
@@ -336,7 +332,9 @@ function PrivateApp() {
|
|||||||
onSceneChange={handleSceneChange}
|
onSceneChange={handleSceneChange}
|
||||||
onSelectionStateChange={handleCodeBlockSelectionChange}
|
onSelectionStateChange={handleCodeBlockSelectionChange}
|
||||||
onEditorActivity={scheduleThemeTokenSync}
|
onEditorActivity={scheduleThemeTokenSync}
|
||||||
onExcalidrawAPI={handleExcalidrawAPI}
|
onExcalidrawAPI={(api) => {
|
||||||
|
excalidrawApiRef.current = api;
|
||||||
|
}}
|
||||||
renderEmbeddable={renderCodeBlockEmbeddable}
|
renderEmbeddable={renderCodeBlockEmbeddable}
|
||||||
/>
|
/>
|
||||||
<CodeBlockSidebar
|
<CodeBlockSidebar
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
import type { ExcalidrawProps } from "@excalidraw/excalidraw/types";
|
import type { ExcalidrawProps } from "@excalidraw/excalidraw/types";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawEmbeddableElement,
|
ExcalidrawEmbeddableElement,
|
||||||
@@ -57,7 +58,7 @@ const CodeBlockEmbeddable = memo(function CodeBlockEmbeddable({
|
|||||||
{html ? (
|
{html ? (
|
||||||
<div
|
<div
|
||||||
className="codeblock-embeddable-html"
|
className="codeblock-embeddable-html"
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<pre className="codeblock-embeddable-fallback">
|
<pre className="codeblock-embeddable-fallback">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
import {
|
import {
|
||||||
CODE_BLOCK_LANGUAGES,
|
CODE_BLOCK_LANGUAGES,
|
||||||
type CodeBlockDraftState,
|
type CodeBlockDraftState,
|
||||||
@@ -75,7 +76,7 @@ function CodeBlockEditor({
|
|||||||
{html ? (
|
{html ? (
|
||||||
<div
|
<div
|
||||||
className="codeblock-editor-highlight-html"
|
className="codeblock-editor-highlight-html"
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<pre className="codeblock-editor-highlight-fallback">
|
<pre className="codeblock-editor-highlight-fallback">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback } from "react";
|
import { memo } from "react";
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
@@ -7,7 +7,6 @@ import type {
|
|||||||
ExcalidrawInitialDataState,
|
ExcalidrawInitialDataState,
|
||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
|
||||||
import "../../../node_modules/@excalidraw/excalidraw/dist/prod/index.css";
|
import "../../../node_modules/@excalidraw/excalidraw/dist/prod/index.css";
|
||||||
import { type ScenePayload } from "../../core/shared";
|
import { type ScenePayload } from "../../core/shared";
|
||||||
import {
|
import {
|
||||||
@@ -57,19 +56,6 @@ export const EditorCanvas = memo(function EditorCanvas({
|
|||||||
onExcalidrawAPI,
|
onExcalidrawAPI,
|
||||||
renderEmbeddable,
|
renderEmbeddable,
|
||||||
}: EditorCanvasProps) {
|
}: EditorCanvasProps) {
|
||||||
const handleChange = useCallback(
|
|
||||||
(
|
|
||||||
elements: readonly OrderedExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
files: BinaryFiles,
|
|
||||||
) => {
|
|
||||||
onEditorActivity();
|
|
||||||
onSceneChange(sceneFromEditor(elements, appState, files));
|
|
||||||
onSelectionStateChange(getCodeBlockSelectionState(elements, appState));
|
|
||||||
},
|
|
||||||
[onEditorActivity, onSceneChange, onSelectionStateChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading || !scene) {
|
if (loading || !scene) {
|
||||||
return <div className="editor-loading">{error ?? "Loading..."}</div>;
|
return <div className="editor-loading">{error ?? "Loading..."}</div>;
|
||||||
}
|
}
|
||||||
@@ -81,7 +67,13 @@ export const EditorCanvas = memo(function EditorCanvas({
|
|||||||
initialData={sceneToInitialData(scene)}
|
initialData={sceneToInitialData(scene)}
|
||||||
excalidrawAPI={onExcalidrawAPI}
|
excalidrawAPI={onExcalidrawAPI}
|
||||||
renderEmbeddable={renderEmbeddable}
|
renderEmbeddable={renderEmbeddable}
|
||||||
onChange={handleChange}
|
onChange={(elements, appState, files) => {
|
||||||
|
onEditorActivity();
|
||||||
|
onSceneChange(sceneFromEditor(elements, appState, files));
|
||||||
|
onSelectionStateChange(
|
||||||
|
getCodeBlockSelectionState(elements, appState),
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user