Skip to content

fix(client): replace innerHTML and dangerouslySetInnerHTML with safe alternatives#41836

Open
subrata71 wants to merge 1 commit into
releasefrom
fix/xss-innerhtml-dangerously-set-ghsa-frqc-vqqg
Open

fix(client): replace innerHTML and dangerouslySetInnerHTML with safe alternatives#41836
subrata71 wants to merge 1 commit into
releasefrom
fix/xss-innerhtml-dangerously-set-ghsa-frqc-vqqg

Conversation

@subrata71
Copy link
Copy Markdown
Collaborator

@subrata71 subrata71 commented May 21, 2026

Description

Replace unsafe DOM manipulation patterns across 5 client-side files to close two security advisories:

GHSA-frqc-9x9h-fv9v — CSS token injection via innerHTML on <style> elements in custom widget scripts. Replaced with textContent, which sets raw text without HTML parsing and prevents </style> breakout attacks.

GHSA-vqqg-rp36-9c62dangerouslySetInnerHTML usage in 3 UI components. Fixed per content type:

  • ternDocTooltip.tsx: Replaced with React text children {doc} (content is always plain text from Tern.js !doc metadata)
  • CrudInfoModal.tsx: Replaced with Interweave component (content contains intentional <b> tags from server)
  • ManualUpgrades.tsx: Replaced with Interweave component (content contains intentional <br>, <code>, <strong> tags)

No new dependencies added — Interweave is already used in HTMLCell for table widget HTML rendering.

Files changed (production)

  • app/client/src/widgets/wds/WDSCustomWidget/component/customWidgetscript.js — 3 innerHTML → textContent
  • app/client/src/widgets/CustomWidget/component/customWidgetscript.js — 1 innerHTML → textContent
  • app/client/src/utils/autocomplete/ternDocTooltip.tsx — dangerouslySetInnerHTML → React text children
  • app/client/src/pages/Editor/GeneratePage/components/CrudInfoModal.tsx — dangerouslySetInnerHTML → Interweave
  • app/client/src/components/BottomBar/ManualUpgrades.tsx — dangerouslySetInnerHTML → Interweave

Files added (tests)

  • generateAppsmithCssVariables.test.js — tests both WDS and legacy custom widget CSS variable generation including XSS prevention
  • ternDocTooltip.test.tsx — tests autocomplete tooltip rendering and XSS prevention
  • CrudInfoModal.test.tsx — tests InfoContent rendering and XSS prevention
  • ManualUpgrades.test.tsx — tests UpdatesModal rendering and XSS prevention

TDD verification

  • RED: 5 XSS tests failed against current code, confirming vulnerabilities
  • GREEN: All 19 tests pass after applying fixes
  • Existing table widget tests (BasicCell, HTMLCell) continue to pass

Automation

/ok-to-test tags="@tag.All"

🔍 Cypress test results

Communication

Should the DevRel and Marketing teams inform users about this change?

  • Yes
  • No

Tip

🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/26435948997
Commit: 2b09a73
Cypress dashboard.
Tags: @tag.All
Spec:


Tue, 26 May 2026 10:16:47 UTC

Summary by CodeRabbit

  • Bug Fixes

    • Improved content rendering methods in update modals, information dialogs, and code tooltips.
    • Enhanced CSS styling application in custom widgets to use safer content assignment techniques.
  • Tests

    • Added test coverage for content rendering behavior across modals, dialogs, and tooltips.
    • Introduced regression tests for CSS variable generation and styling functionality.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

Walkthrough

This PR replaces unsafe HTML injection patterns: component HTML rendering now uses Interweave or plain JSX (no dangerouslySetInnerHTML), tooltip docs render as plain text, and generated STYLE content uses textContent instead of innerHTML. XSS-focused tests are added across components and widget CSS generation.

Changes

Security hardening - Remove dangerous HTML injection patterns

Layer / File(s) Summary
UpdatesModal Interweave migration
app/client/src/components/BottomBar/ManualUpgrades.tsx, app/client/src/components/BottomBar/__tests__/ManualUpgrades.test.tsx
UpdatesModal now uses Interweave to render update.description and update.disclaimer.desc instead of dangerouslySetInnerHTML; component is exported. XSS tests verify safe rendering of benign HTML and blocking of <script> and <img onerror> payloads.
CrudInfoModal Interweave migration
app/client/src/pages/Editor/GeneratePage/components/CrudInfoModal.tsx, app/client/src/pages/Editor/GeneratePage/components/__tests__/CrudInfoModal.test.tsx
InfoContent switches from dangerouslySetInnerHTML to Interweave for successMessage rendering and is exported. Tests verify safe HTML tag rendering and suppression of script/img onerror injection.
TernDocToolTip plain text rendering
app/client/src/utils/autocomplete/ternDocTooltip.tsx, app/client/src/utils/autocomplete/__tests__/ternDocTooltip.test.tsx
Tooltip documentation renders doc directly as JSX inside a <pre> instead of injecting HTML; tests cover normal docs, XSS-like payloads, and empty-doc cases.
CSS variable generation textContent migration
app/client/src/widgets/CustomWidget/component/customWidgetscript.js, app/client/src/widgets/wds/WDSCustomWidget/component/customWidgetscript.js, app/client/src/widgets/__tests__/generateAppsmithCssVariables.test.js
Both custom widget implementations write CSS token content to STYLE elements via textContent instead of innerHTML. Tests assert STYLE creation/reuse, primitive-only token inclusion, and that Element.prototype.innerHTML is never used for STYLE updates even with malicious inputs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

Sanitize the text, deny the sly script,
Interweave we trust, no innerHTML crypt,
Styles set as text, not parsed or slipped,
Tests guard the gates where payloads might drift,
Code clearer and safer — a small, steady lift.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main security-focused change: replacing unsafe DOM manipulation patterns (innerHTML, dangerouslySetInnerHTML) with safe alternatives across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description comprehensively covers security fixes, implementation details, test coverage, and verification with linked advisories.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/xss-innerhtml-dangerously-set-ghsa-frqc-vqqg

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.

🧹 Nitpick comments (4)
app/client/src/pages/Editor/GeneratePage/components/CrudInfoModal.tsx (1)

109-111: 💤 Low value

Heads-up: Interweave defaults to wrapping output in a <span>.

InfoContentHeadingText is itself a styled <span>, so the rendered markup becomes <span><span>…</span></span>. Harmless, but if you’d prefer flat output, pass tagName="fragment"/noWrap (depending on Interweave version) or render directly into a <div>.

🤖 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 `@app/client/src/pages/Editor/GeneratePage/components/CrudInfoModal.tsx` around
lines 109 - 111, The Interweave output is wrapped in a <span>, causing nested
spans because InfoContentHeadingText is a styled <span>; update the rendering in
CrudInfoModal to avoid the extra wrapper by passing Interweave props to render a
fragment/no wrap (e.g., tagName="fragment" or noWrap depending on your
Interweave version) or replace InfoContentHeadingText with a block element
(e.g., a styled <div>) and render <Interweave content={successMessage} />
directly; locate the usage in the component where InfoContentHeadingText wraps
Interweave and apply one of these changes to produce flat output.
app/client/src/widgets/__tests__/generateAppsmithCssVariables.test.js (2)

69-106: ⚡ Quick win

Consider adding behavior-level XSS assertions alongside the innerHTML spy.

The spy approach is a fair workaround for jsdom’s <style> parsing limitation, but it only proves "implementation does not call innerHTML" — it would silently pass if a future refactor switched to e.g. insertAdjacentHTML or Range.createContextualFragment. A cheap complement is to also assert that the malicious payload is preserved as raw textContent and no stray <script>/<img> ended up in the document.

♻️ Suggested additional assertions
     const styleSetterCalls = spy.mock.contexts.filter(
       (ctx) => ctx?.tagName === "STYLE",
     );

     expect(styleSetterCalls).toHaveLength(0);
+    const style = document.getElementById(`appsmith-${provider}-css-tokens`);
+    expect(style.textContent).toContain("</style><script>alert(1)</script>");
+    expect(document.querySelectorAll("script")).toHaveLength(0);
🤖 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 `@app/client/src/widgets/__tests__/generateAppsmithCssVariables.test.js` around
lines 69 - 106, Add behavior-level XSS assertions to the existing tests that spy
on Element.prototype.innerHTML: after calling apply = fn(provider); apply({...})
assert that the malicious payload remains as raw textContent on the affected
<style> node and that no <script> or <img> elements were actually appended to
the document (e.g. search document.querySelectorAll("script")/("img") or inspect
the created style element's textContent for the payload). Keep these checks
alongside the existing styleSetterCalls assertions so the tests fail if a
refactor uses insertAdjacentHTML/Range.createContextualFragment or otherwise
injects nodes instead of preserving textContent.

14-21: 💤 Low value

Minor: scoped DOM reset is safer than wiping document.head.

document.head.innerHTML = "" between every test nukes anything jsdom or other setup scripts may have added to <head> (e.g., RTL’s injected styles, jest-dom global setup). Removing only the generated <style id="appsmith-*-css-tokens"> keeps the test hermetic without touching unrelated head children.

♻️ Suggested scoped cleanup
     beforeEach(() => {
-      document.head.innerHTML = "";
-      document.body.innerHTML = "";
+      document
+        .querySelectorAll('[id^="appsmith-"][id$="-css-tokens"]')
+        .forEach((el) => el.remove());
     });
🤖 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 `@app/client/src/widgets/__tests__/generateAppsmithCssVariables.test.js` around
lines 14 - 21, Replace the global head wipe in the test setup with a scoped
removal of the generated style token elements: instead of clearing
document.head.innerHTML in the beforeEach, locate and remove any <style>
elements whose id matches the generated pattern (e.g., ids starting with
"appsmith-" and ending with "-css-tokens") so you only remove the test-injected
CSS tokens while preserving other jsdom/jest/RTL head injections; keep the
afterEach jest.restoreAllMocks() as-is.
app/client/src/utils/autocomplete/__tests__/ternDocTooltip.test.tsx (1)

36-45: 💤 Low value

Strengthen the XSS assertion with a positive check on escaped text.

Asserting only the absence of <img> passes even if the component renders nothing at all. A complementary positive assertion that the raw payload appears as escaped text content makes the regression test more durable against future refactors.

♻️ Suggested tightening
     const imgElements = container.querySelectorAll("img");

     expect(imgElements).toHaveLength(0);
+    expect(container.querySelector("pre")).toHaveTextContent(xssPayload);
🤖 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 `@app/client/src/utils/autocomplete/__tests__/ternDocTooltip.test.tsx` around
lines 36 - 45, The test only asserts no <img> nodes exist but should also
positively assert the XSS payload is rendered as escaped text; update the test
for TernDocToolTip (in ternDocTooltip.test.tsx) that uses
makeCompletion(xssPayload) to check container.textContent or a getByText query
contains the literal string '<img src=x onerror="alert(1)">' (or otherwise
assert innerHTML does not include an unescaped "<img" while textContent includes
the payload) so the component is verified to render the payload as escaped text
rather than silently render nothing.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@app/client/src/pages/Editor/GeneratePage/components/CrudInfoModal.tsx`:
- Around line 109-111: The Interweave output is wrapped in a <span>, causing
nested spans because InfoContentHeadingText is a styled <span>; update the
rendering in CrudInfoModal to avoid the extra wrapper by passing Interweave
props to render a fragment/no wrap (e.g., tagName="fragment" or noWrap depending
on your Interweave version) or replace InfoContentHeadingText with a block
element (e.g., a styled <div>) and render <Interweave content={successMessage}
/> directly; locate the usage in the component where InfoContentHeadingText
wraps Interweave and apply one of these changes to produce flat output.

In `@app/client/src/utils/autocomplete/__tests__/ternDocTooltip.test.tsx`:
- Around line 36-45: The test only asserts no <img> nodes exist but should also
positively assert the XSS payload is rendered as escaped text; update the test
for TernDocToolTip (in ternDocTooltip.test.tsx) that uses
makeCompletion(xssPayload) to check container.textContent or a getByText query
contains the literal string '<img src=x onerror="alert(1)">' (or otherwise
assert innerHTML does not include an unescaped "<img" while textContent includes
the payload) so the component is verified to render the payload as escaped text
rather than silently render nothing.

In `@app/client/src/widgets/__tests__/generateAppsmithCssVariables.test.js`:
- Around line 69-106: Add behavior-level XSS assertions to the existing tests
that spy on Element.prototype.innerHTML: after calling apply = fn(provider);
apply({...}) assert that the malicious payload remains as raw textContent on the
affected <style> node and that no <script> or <img> elements were actually
appended to the document (e.g. search
document.querySelectorAll("script")/("img") or inspect the created style
element's textContent for the payload). Keep these checks alongside the existing
styleSetterCalls assertions so the tests fail if a refactor uses
insertAdjacentHTML/Range.createContextualFragment or otherwise injects nodes
instead of preserving textContent.
- Around line 14-21: Replace the global head wipe in the test setup with a
scoped removal of the generated style token elements: instead of clearing
document.head.innerHTML in the beforeEach, locate and remove any <style>
elements whose id matches the generated pattern (e.g., ids starting with
"appsmith-" and ending with "-css-tokens") so you only remove the test-injected
CSS tokens while preserving other jsdom/jest/RTL head injections; keep the
afterEach jest.restoreAllMocks() as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c03a4ddb-8c6e-40a9-b3f3-5fdaf0895d46

📥 Commits

Reviewing files that changed from the base of the PR and between d44df5f and 25a37cb.

📒 Files selected for processing (9)
  • app/client/src/components/BottomBar/ManualUpgrades.tsx
  • app/client/src/components/BottomBar/__tests__/ManualUpgrades.test.tsx
  • app/client/src/pages/Editor/GeneratePage/components/CrudInfoModal.tsx
  • app/client/src/pages/Editor/GeneratePage/components/__tests__/CrudInfoModal.test.tsx
  • app/client/src/utils/autocomplete/__tests__/ternDocTooltip.test.tsx
  • app/client/src/utils/autocomplete/ternDocTooltip.tsx
  • app/client/src/widgets/CustomWidget/component/customWidgetscript.js
  • app/client/src/widgets/__tests__/generateAppsmithCssVariables.test.js
  • app/client/src/widgets/wds/WDSCustomWidget/component/customWidgetscript.js

@subrata71 subrata71 self-assigned this May 21, 2026
@subrata71 subrata71 added the ok-to-test Required label for CI label May 21, 2026
…alternatives

Replace unsafe DOM manipulation patterns to prevent XSS:

- Custom widget scripts: replace innerHTML with textContent on <style>
  elements to prevent </style> breakout attacks (GHSA-frqc-9x9h-fv9v)
- ternDocTooltip: replace dangerouslySetInnerHTML with React text children
  since doc content is always plain text (GHSA-vqqg-rp36-9c62)
- CrudInfoModal/ManualUpgrades: replace dangerouslySetInnerHTML with
  Interweave component for safe HTML rendering (GHSA-vqqg-rp36-9c62)
@subrata71 subrata71 force-pushed the fix/xss-innerhtml-dangerously-set-ghsa-frqc-vqqg branch from 25a37cb to 2b09a73 Compare May 26, 2026 06:21
@subrata71 subrata71 requested a review from ashit-rath May 26, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok-to-test Required label for CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant