Skip to content
Merged
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
62 changes: 22 additions & 40 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,18 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
uses: actions/checkout@v7

- name: Setup uv
uses: astral-sh/setup-uv@v7

- name: Install Python
run: uv python install "$(cat .python-version)"

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6

- name: Install frontend dependencies
- name: Install frontend dependencies and Node runtime
run: pnpm install --frozen-lockfile

- name: Install Python dependencies
Expand Down Expand Up @@ -72,33 +65,29 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.11", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
uses: actions/checkout@v7

- name: Setup uv
uses: astral-sh/setup-uv@v7

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
- name: Install Python
run: uv python install "${{ matrix.python-version }}"

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6

- name: Install frontend dependencies and Node runtime
run: pnpm install --frozen-lockfile

- name: Install Python test dependencies
run: uv sync --group test --no-default-groups
run: uv sync --python "${{ matrix.python-version }}" --group test --no-default-groups

- name: Run non-e2e tests
run: uv run pytest -q -m "not e2e"
run: uv run --python "${{ matrix.python-version }}" pytest -q -m "not e2e"

playwright-e2e:
runs-on: ubuntu-latest
Expand All @@ -108,25 +97,18 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
uses: actions/checkout@v7

- name: Setup uv
uses: astral-sh/setup-uv@v7

- name: Install Python
run: uv python install "$(cat .python-version)"

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6

- name: Install frontend dependencies
- name: Install frontend dependencies and Node runtime
run: pnpm install --frozen-lockfile

- name: Install Python dependencies
Expand Down
23 changes: 8 additions & 15 deletions .github/workflows/playground-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,18 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
uses: actions/checkout@v7

- name: Setup `uv`
uses: astral-sh/setup-uv@v7

- name: Install Python
run: uv python install "$(cat .python-version)"

- name: Setup `pnpm`
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/playground-deploy.yml

Repository: peter-gy/d2-widget

Length of output: 1852


Pin deploy workflow actions to immutable SHAs.

The current tagged references (@v6, @v5) are mutable and leave the Pages pipeline open to action-retag supply-chain risk. Replace with full commit SHAs:

  • Line 28: pnpm/action-setup@v6 → pinned SHA
  • Line 41: actions/upload-pages-artifact@v5 → pinned SHA
  • Line 59: actions/deploy-pages@v5 → pinned SHA
🧰 Tools
🪛 zizmor (1.26.1)

[error] 28-28: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/playground-deploy.yml at line 28, Replace the mutable
version tag references in the GitHub Actions workflow with their corresponding
full commit SHAs to mitigate supply-chain risk. For the pnpm/action-setup action
on line 28, replace the `@v6` tag with its pinned SHA. Similarly, replace the `@v5`
tag for actions/upload-pages-artifact and actions/deploy-pages on lines 41 and
59 respectively with their respective pinned commit SHAs. Each action should
reference a specific immutable commit hash instead of a mutable version tag.

Source: Linters/SAST tools


- name: Install dependencies
- name: Install dependencies and Node runtime
run: |
pnpm install --frozen-lockfile
uv sync --all-groups
Expand All @@ -45,7 +38,7 @@ jobs:
uv run --project .. marimo export html-wasm d2_playground.py -o dist --mode run

- name: Upload Pages Artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: playground/dist

Expand All @@ -63,6 +56,6 @@ jobs:
steps:
- name: 🌐 Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
with:
artifact_name: github-pages
76 changes: 52 additions & 24 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Upload Python Package
name: Publish package

on:
release:
Expand All @@ -11,61 +11,89 @@ jobs:
qa:
uses: ./.github/workflows/lint.yml

release-build:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
needs:
- qa

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Setup Python
uses: actions/setup-python@v6
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
python-version-file: ".python-version"
enable-cache: true
cache-dependency-glob: uv.lock

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
- name: Install Python
run: uv python install "$(cat .python-version)"

- name: Setup `uv`
uses: astral-sh/setup-uv@v7

- name: Setup `pnpm`
uses: pnpm/action-setup@v4
- name: Setup pnpm
uses: pnpm/action-setup@v6

- name: Build release distributions
run: |
pnpm install --frozen-lockfile
uv build

- name: Upload distributions
- name: Upload package artifact
uses: actions/upload-artifact@v7
with:
name: release-dists
name: dist
path: dist/
retention-days: 1
if-no-files-found: error

pypi-publish:
publish:
runs-on: ubuntu-latest
needs:
- release-build
- build
permissions:
contents: read
id-token: write

environment:
name: pypi
url: https://pypi.org/project/d2-widget/${{ github.event.release.name }}

steps:
- name: Retrieve release distributions
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup uv
uses: astral-sh/setup-uv@v7

- name: Download package artifact
uses: actions/download-artifact@v8
with:
name: release-dists
name: dist
path: dist/

- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Publish package
run: uv publish --trusted-publishing always

release-notes:
runs-on: ubuntu-latest
needs:
- publish
permissions:
contents: write

steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
packages-dir: dist/
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v6

- name: Install Node runtime
run: pnpm install --frozen-lockfile

- name: Update release notes
run: pnpm dlx changelogithub@14.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

1 change: 1 addition & 0 deletions js/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.css";
6 changes: 6 additions & 0 deletions js/widget.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
width: 100%;
}

.d2-widget svg {
display: block;
max-width: 100%;
height: auto;
}

.error {
font-family: Consolas, monospace;
padding: 10px;
Expand Down
34 changes: 20 additions & 14 deletions js/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async function diagramToSvg(d2: D2, diagram: string, options: CompileOptions) {
export default () => {
const d2 = new D2();
let isRendering = false;
let renderAgain = false;

return {
async render({ model, el }: RenderProps<Model>) {
Expand All @@ -57,27 +58,32 @@ export default () => {

// Diagramming logic
const update = async () => {
// If another update is already in progress, do nothing
if (isRendering) {
console.log("Another update is already in progress, skipping");
renderAgain = true;
return;
}

isRendering = true;
try {
const svg = await diagramToSvg(d2, getDiagram(), getOptions());
setSvg(svg);
getRoot().innerHTML = svg;
} catch (error: unknown) {
console.error(error);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
const root = getRoot();
const errorElement = document.createElement("div");
errorElement.className = "error";
errorElement.textContent = `Error generating diagram: ${errorMessage}`;
root.replaceChildren(errorElement);
do {
renderAgain = false;
try {
const svg = await diagramToSvg(d2, getDiagram(), getOptions());
setSvg(svg);
getRoot().innerHTML = svg;
} catch (error: unknown) {
Comment on lines +71 to +74

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does @terrastruct/d2v0.1.33 guarantee thatd2.render(...)SVG output is sanitized and safe for direct insertion viaelement.innerHTML?

💡 Result:

No, @terrastruct/d2 v0.1.33 does not provide a guarantee that the output of d2.render(...) is sanitized or inherently safe for direct insertion via element.innerHTML [1][2][3]. The d2.render function generates an SVG string [1][2]. Inserting any string directly into element.innerHTML creates a potential cross-site scripting (XSS) security risk because innerHTML is an injection sink [4]. If the D2 input or the resulting SVG contains untrusted or malicious content, simply injecting it into the DOM without sanitization can lead to XSS attacks [4][5]. Security best practices for handling SVG output include: 1. Sanitization: Always pass the output through a dedicated sanitization library (such as DOMPurify) before setting it on element.innerHTML to remove potentially malicious elements or attributes [4][5]. 2. Contextual Safety: The library itself is focused on diagram generation, not security hardening of the output for arbitrary web contexts [1][3]. While D2 does include features like escaping query parameters in icons to valid SVG XML [6], this does not constitute a full security guarantee for arbitrary insertion into the DOM. 3. Content Security Policy (CSP): Implementing a strong CSP can help mitigate risks, but it is a defense-in-depth measure, not a replacement for proper sanitization when using sinks like innerHTML [7][4]. For secure insertion, you should treat the output of d2.render as untrusted input and apply appropriate sanitization routines before rendering it [4].

Citations:


🏁 Script executed:

# First, let's find and inspect the widget.ts file
fd -t f "widget.ts" --type f

Repository: peter-gy/d2-widget

Length of output: 169


🏁 Script executed:

# Check if package.json exists and look for DOMPurify dependency
fd -t f "package.json" --type f | head -5

Repository: peter-gy/d2-widget

Length of output: 169


🏁 Script executed:

# Let's also check the project structure
git ls-files | grep -E "(widget|package)" | head -20

Repository: peter-gy/d2-widget

Length of output: 342


🏁 Script executed:

# Read the widget.ts file with line numbers to verify the vulnerable code
cat -n js/widget.ts | head -100

Repository: peter-gy/d2-widget

Length of output: 3738


🏁 Script executed:

# Read package.json to check dependencies
cat package.json

Repository: peter-gy/d2-widget

Length of output: 987


Sanitize SVG before DOM injection to prevent XSS.

Line 73 directly injects SVG via innerHTML without sanitization. Since the diagram content originates from editable user input, untrusted content can flow into this path and execute script-capable SVG payloads. Use DOMPurify to sanitize the output before insertion.

Suggested fix
+import DOMPurify from "dompurify";
@@
-							const svg = await diagramToSvg(d2, getDiagram(), getOptions());
-							setSvg(svg);
-							getRoot().innerHTML = svg;
+							const svg = await diagramToSvg(d2, getDiagram(), getOptions());
+							const safeSvg = DOMPurify.sanitize(svg, {
+								USE_PROFILES: { svg: true, svgFilters: true },
+							});
+							setSvg(safeSvg);
+							getRoot().innerHTML = safeSvg;
📝 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
const svg = await diagramToSvg(d2, getDiagram(), getOptions());
setSvg(svg);
getRoot().innerHTML = svg;
} catch (error: unknown) {
const svg = await diagramToSvg(d2, getDiagram(), getOptions());
const safeSvg = DOMPurify.sanitize(svg, {
USE_PROFILES: { svg: true, svgFilters: true },
});
setSvg(safeSvg);
getRoot().innerHTML = safeSvg;
} catch (error: unknown) {
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 72-72: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: getRoot().innerHTML = svg
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(unsafe-html-content-assignment)


[warning] 72-72: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: getRoot().innerHTML = svg
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(dom-content-modification)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/widget.ts` around lines 71 - 74, The line where getRoot().innerHTML is set
directly to the svg variable lacks sanitization, creating an XSS vulnerability
since the diagram content comes from user input. Import DOMPurify at the top of
the file and modify the innerHTML assignment to sanitize the svg content by
passing it through DOMPurify.sanitize() before injection, ensuring malicious
script payloads in the SVG are neutralized before being inserted into the DOM.

Source: Linters/SAST tools

console.error(error);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
const root = getRoot();
const errorElement = document.createElement("div");
errorElement.className = "error";
errorElement.textContent = `Error generating diagram: ${errorMessage}`;
root.replaceChildren(errorElement);
}
} while (renderAgain);
} finally {
isRendering = false;
}
isRendering = false;
};

// Set up root element
Expand Down
25 changes: 12 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@
"lint": "oxlint . --deny-warnings"
},
"devDependencies": {
"@anywidget/types": "^0.2.0",
"@anywidget/types": "^0.4.0",
"@terrastruct/d2": "0.1.33",
"esbuild": "^0.27.3",
"oxfmt": "0.35.x",
"oxlint": "^1.50.0",
"typescript": "^5.9.3"
"esbuild": "^0.28.1",
"oxfmt": "0.55.0",
"oxlint": "^1.70.0",
"typescript": "^6.0.3"
},
"engines": {
"node": "24.x"
"devEngines": {
"runtime": {
"name": "node",
"version": ">=24",
"onFail": "download"
}
},
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a",
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
}
Loading