Skip to content

Security: Unsanitized SVG content injected via dangerouslySetInnerHTML (XSS risk)#647

Open
tuanaiseo wants to merge 1 commit intositeboon:mainfrom
tuanaiseo:contribai/fix/security/unsanitized-svg-content-injected-via-dan
Open

Security: Unsanitized SVG content injected via dangerouslySetInnerHTML (XSS risk)#647
tuanaiseo wants to merge 1 commit intositeboon:mainfrom
tuanaiseo:contribai/fix/security/unsanitized-svg-content-injected-via-dan

Conversation

@tuanaiseo
Copy link
Copy Markdown

@tuanaiseo tuanaiseo commented Apr 11, 2026

Problem

The plugin icon renderer fetches SVG text from /api/plugins/.../assets/... and injects it directly into the DOM using dangerouslySetInnerHTML after only checking that the payload starts with <svg. This does not remove malicious attributes/elements (e.g., event handlers, scriptable SVG payloads), enabling DOM-based XSS if a plugin asset is malicious or compromised.

Severity: high
File: src/components/plugins/view/PluginIcon.tsx

Solution

Sanitize SVG content before injection using a strict sanitizer (e.g., DOMPurify with SVG profile and forbidden tags/attributes), or render icons as safe image sources (<img src=...>) with server-side content-type validation and CSP hardening. Avoid dangerouslySetInnerHTML for untrusted plugin-provided markup.

Changes

  • src/components/plugins/view/PluginIcon.tsx (modified)

Testing

  • Existing tests pass
  • Manual review completed
  • No new warnings/errors introduced

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced plugin icon security by implementing SVG validation and sanitization that removes dangerous elements, scripts, embedded content, and event handlers while maintaining normal icon display and functionality.

…erouslys

The plugin icon renderer fetches SVG text from `/api/plugins/.../assets/...` and injects it directly into the DOM using `dangerouslySetInnerHTML` after only checking that the payload starts with `<svg`. This does not remove malicious attributes/elements (e.g., event handlers, scriptable SVG payloads), enabling DOM-based XSS if a plugin asset is malicious or compromised.

Affected files: PluginIcon.tsx

Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

The PluginIcon component now includes SVG sanitization logic that parses fetched SVG content, removes dangerous elements and unsafe attributes, and validates the result before rendering to prevent security risks.

Changes

Cohort / File(s) Summary
SVG Sanitization
src/components/plugins/view/PluginIcon.tsx
Added sanitizeSvg() helper function that removes malicious elements (script, foreignObject, iframe, object, embed, link, meta, style) and unsafe attributes (event handlers, href, xlink:href, JavaScript URLs). Updated icon fetch logic to require successful sanitization before caching and rendering SVG content.

Poem

🐰 SVGs once risky, now safe and clean,
Dangerous scripts stripped from the scene,
Malicious elements vanish with care,
PluginIcon now handles content fair!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main security vulnerability being fixed: unsanitized SVG content injected via dangerouslySetInnerHTML causing an XSS risk, which directly matches the primary change in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/components/plugins/view/PluginIcon.tsx (2)

35-36: Consider using localName for more robust namespace handling.

The xlink:href attribute check relies on the qualified name. While this works in modern browsers, using attr.localName === 'href' is more robust for namespaced attributes, as namespace prefixes can vary.

♻️ Suggested improvement
         const name = attr.name.toLowerCase();
+        const localName = attr.localName?.toLowerCase() ?? name;
         const value = attr.value.trim().toLowerCase();
         if (
           name.startsWith('on') ||
-          name === 'href' ||
-          name === 'xlink:href' ||
+          localName === 'href' ||
           value.startsWith('javascript:')
         ) {
           el.removeAttribute(attr.name);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/plugins/view/PluginIcon.tsx` around lines 35 - 36, The
attribute check in PluginIcon.tsx that tests for name === 'href' || name ===
'xlink:href' is fragile; update the logic to use the attribute object's
localName (e.g., attr.localName === 'href') so namespaced prefixes are handled
robustly. Locate the check inside the PluginIcon component (the function that
iterates SVG attributes) and replace the qualified-name comparisons with a
single localName comparison, keeping existing behavior for truthy/non-empty
values and ensuring any code paths that relied on 'xlink:href' continue to run
when localName === 'href'.

13-48: Consider using DOMPurify for SVG sanitization instead of custom implementation.

Custom SVG sanitizers are prone to bypasses due to SVG's large attack surface. The current implementation has specific limitations:

  • Removes all href and xlink:href attributes, breaking legitimate SVG links and image references
  • Doesn't handle data URI protocols (e.g., data:text/html), which can still be malicious
  • Doesn't strip inline style attributes
  • May miss edge cases in namespace or protocol handling

DOMPurify is battle-tested, actively maintained against new bypass techniques, and handles these cases correctly. It would require adding DOMPurify as a dependency.

♻️ Recommended approach using DOMPurify
import DOMPurify from 'dompurify';

function sanitizeSvg(svgText: string): string | null {
  try {
    const clean = DOMPurify.sanitize(svgText, {
      USE_PROFILES: { svg: true, svgFilters: false },
      FORBID_TAGS: ['foreignObject', 'style'],
      FORBID_ATTR: ['style'],
    });
    // Ensure result is an SVG
    if (!clean.trim().startsWith('<svg')) return null;
    return clean;
  } catch {
    return null;
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/plugins/view/PluginIcon.tsx` around lines 13 - 48, The
sanitizeSvg function uses a fragile custom sanitizer; replace its logic with
DOMPurify: add DOMPurify as a dependency and in sanitizeSvg call
DOMPurify.sanitize(svgText, { USE_PROFILES: { svg: true, svgFilters: false },
FORBID_TAGS: ['foreignObject','style'], FORBID_ATTR: ['style'] }) (or equivalent
config) then verify the cleaned output starts with an <svg> root and return it
or null on failure; remove the manual DOMParser/TreeWalker/attribute-removal
code in sanitizeSvg and ensure data: and javascript: protocols are handled by
DOMPurify configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/plugins/view/PluginIcon.tsx`:
- Around line 19-21: The SVG sanitizer in PluginIcon.tsx currently removes
elements via
doc.querySelectorAll('script,foreignObject,iframe,object,embed,link,meta,style')
but omits SVG animation elements which can inject event handlers; update the
selector used on the parsed document (the call where doc.querySelectorAll(...)
is invoked inside the PluginIcon component/cleanup logic) to also remove
'animate', 'set', and 'animateTransform' nodes so those SVG animation elements
are stripped before insertion.

---

Nitpick comments:
In `@src/components/plugins/view/PluginIcon.tsx`:
- Around line 35-36: The attribute check in PluginIcon.tsx that tests for name
=== 'href' || name === 'xlink:href' is fragile; update the logic to use the
attribute object's localName (e.g., attr.localName === 'href') so namespaced
prefixes are handled robustly. Locate the check inside the PluginIcon component
(the function that iterates SVG attributes) and replace the qualified-name
comparisons with a single localName comparison, keeping existing behavior for
truthy/non-empty values and ensuring any code paths that relied on 'xlink:href'
continue to run when localName === 'href'.
- Around line 13-48: The sanitizeSvg function uses a fragile custom sanitizer;
replace its logic with DOMPurify: add DOMPurify as a dependency and in
sanitizeSvg call DOMPurify.sanitize(svgText, { USE_PROFILES: { svg: true,
svgFilters: false }, FORBID_TAGS: ['foreignObject','style'], FORBID_ATTR:
['style'] }) (or equivalent config) then verify the cleaned output starts with
an <svg> root and return it or null on failure; remove the manual
DOMParser/TreeWalker/attribute-removal code in sanitizeSvg and ensure data: and
javascript: protocols are handled by DOMPurify configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6b086f71-c90d-4176-acbc-da80ba4edb12

📥 Commits

Reviewing files that changed from the base of the PR and between e2459cb and f705f25.

📒 Files selected for processing (1)
  • src/components/plugins/view/PluginIcon.tsx

Comment on lines +19 to +21
doc
.querySelectorAll('script,foreignObject,iframe,object,embed,link,meta,style')
.forEach((el) => el.remove());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SVG animate/set elements can bypass event handler sanitization.

The removal list is missing animate, set, and animateTransform elements. These SVG animation elements can dynamically set attribute values, including event handlers:

<svg xmlns="http://www.w3.org/2000/svg">
  <rect width="100" height="100">
    <set attributeName="onclick" to="alert('XSS')" begin="0s" fill="freeze"/>
  </rect>
</svg>

When the user clicks the rect, the injected handler executes.

🛡️ Proposed fix to include animation elements
   doc
-    .querySelectorAll('script,foreignObject,iframe,object,embed,link,meta,style')
+    .querySelectorAll('script,foreignObject,iframe,object,embed,link,meta,style,animate,set,animateTransform,animateMotion')
     .forEach((el) => el.remove());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/plugins/view/PluginIcon.tsx` around lines 19 - 21, The SVG
sanitizer in PluginIcon.tsx currently removes elements via
doc.querySelectorAll('script,foreignObject,iframe,object,embed,link,meta,style')
but omits SVG animation elements which can inject event handlers; update the
selector used on the parsed document (the call where doc.querySelectorAll(...)
is invoked inside the PluginIcon component/cleanup logic) to also remove
'animate', 'set', and 'animateTransform' nodes so those SVG animation elements
are stripped before insertion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant