Skip to content

fix(many): sanitize unsafe hrefs, SVG and TruncateText output, and isolate DOMPurify (GHSA-qqj4-cgcr-pcjj)#2655

Merged
git-nandor merged 6 commits into
masterfrom
security-fix-ghsa-qqj4-cgcr-pcjj
Jul 24, 2026
Merged

fix(many): sanitize unsafe hrefs, SVG and TruncateText output, and isolate DOMPurify (GHSA-qqj4-cgcr-pcjj)#2655
git-nandor merged 6 commits into
masterfrom
security-fix-ghsa-qqj4-cgcr-pcjj

Conversation

@git-nandor

@git-nandor git-nandor commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the vulnerabilities tracked in security advisory
GHSA-qqj4-cgcr-pcjj. This hardens Instructure UI against XSS through
unsafe URLs, unsanitized SVG/HTML output, and against prototype
pollution in mergeDeep.

The work is split into four focused commits so each mitigation can be
reviewed independently.

Changes

1. Block javascript: and data: URLs — fix(many)

  • Adds two new utilities to @instructure/ui-utils:
    • safeHref(value, tag, attr) — strips URLs whose scheme is not
      allow-listed (blocks javascript:, data:, etc.) using DOMPurify,
      and logs [InstUI] Blocked unsafe <tag> <attr>: to the console when
      it rejects a value.
    • safeLinkProps(...) — convenience wrapper for link-like props.
  • Applies sanitization to the user-controllable URL props of Link,
    Menu/MenuItem, AppNav items, SideNavBar items, BaseButton
    and FileDrop.
  • Sanitizes Avatar's src (and escapes it before interpolating into
    the background-image url(...)).
  • Adds sanitizeSvg to @instructure/ui-svg-images and sanitizes
    InlineSVG's src/<svg> content with DOMPurify.
  • Hardens the mergeDeep util so it no longer copies constructor or
    __proto__ keys (prototype-pollution guard).

2. Expand the safeHref scheme allow-list — fix(ui-utils)

  • Widens the set of URL schemes safeHref accepts so legitimate links
    (e.g. tel:, mailto:, and other expected protocols) keep working
    while the dangerous schemes stay blocked.

3. Replace innerHTML with manual DOM construction in TruncateText — fix(ui-truncate-text,ui-scripts) — INSTUI-5032

  • TruncateText (v1 and v2) no longer builds an HTML string and assigns
    it via innerHTML. It now constructs nodes directly with
    document.createElement / setAttribute / textContent, which is
    inherently XSS-safe.
  • Removes the now-unnecessary escape-html dependency.

4. Isolate DOMPurify config from the shared singleton — fix(ui-utils,ui-svg-images) — INSTUI-5057

  • Stops mutating and depending on the process-wide DOMPurify singleton,
    so InstUI's sanitization config can't leak into (or be affected by)
    other consumers of DOMPurify on the page.

New public API

  • @instructure/ui-utils now exports safeHref and safeLinkProps.
  • @instructure/ui-svg-images gains an internal sanitizeSvg helper.

These are additive; no existing prop, export, theme variable, or default
behavior is removed or changed for valid input, so this is not a
breaking change.

Affected packages

ui-utils, ui-svg-images, ui-truncate-text, ui-avatar,
ui-buttons, ui-file-drop, ui-link, ui-menu, ui-navigation,
ui-side-nav-bar (plus an internal ReDoS hardening in the __docs__
app search).

Testing

  • New unit tests for safeHref, safeLinkProps, sanitizeSvg, and
    mergeDeep.
  • pnpm run test:vitest

Jira

  • INSTUI-5032
  • INSTUI-5057

matyasf and others added 5 commits July 23, 2026 10:26
- sanitize href props and Avatar's src prop from URLs that contain
javascript: or data: with DOMPurify. InstUI will add an error to the
console in this case ([InstUI] Blocked unsafe ${tag} ${attr}:)
- sanitize InlineSVG's src prop (and <svg>) with DOMPurify
- mergeDeep util does not copy constructor or __proto__
Add webcal:, sms:, callto:, cid:, xmpp:, ftps:, feed:, and geo: to
SAFE_SCHEMES. The browser path now also consults this list before
delegating to DOMPurify, so schemes DOMPurify rejects by default (e.g.
webcal:) are no longer dropped. This also fixes silent SSR/browser
drift for sms:, callto:, cid:, xmpp:, and ftps:, which DOMPurify
already allows but the SSR fallback was stripping.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@git-nandor git-nandor self-assigned this Jul 23, 2026
@git-nandor git-nandor changed the title Security fix ghsa qqj4 cgcr pcjj fix(many): sanitize unsafe hrefs, SVG and TruncateText output, and isolate DOMPurify (GHSA-qqj4-cgcr-pcjj) Jul 23, 2026
@git-nandor
git-nandor requested a review from matyasf July 23, 2026 15:32
@git-nandor
git-nandor marked this pull request as ready for review July 23, 2026 15:33
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-24 11:21 UTC

github-actions Bot pushed a commit that referenced this pull request Jul 23, 2026
@github-actions

Copy link
Copy Markdown

Visual regression report

No changes.

Status Count
Unchanged 32
Changed 0
New 0
Removed 0

📊 View full report

Baselines come from the visual-baselines branch. They refresh on every merge to master.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Hardens Instructure UI against XSS vectors and prototype-pollution per advisory GHSA-qqj4-cgcr-pcjj, introducing shared URL/SVG sanitization utilities, removing risky DOM patterns, and tightening deep-merge behavior across affected components/packages.

Changes:

  • Adds safeHref / safeLinkProps to @instructure/ui-utils and applies safer link handling across multiple components.
  • Sanitizes Inline SVG source via a dedicated DOMPurify instance, and removes innerHTML string building from TruncateText v1/v2.
  • Blocks prototype-pollution keys in mergeDeep, and hardens docs search to avoid ReDoS.

Reviewed changes

Copilot reviewed 38 out of 40 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds/removes dependencies to support DOMPurify usage and drop escape-html.
packages/ui-utils/src/safeLinkProps.ts New helper to normalize link-like props (href + default rel).
packages/ui-utils/src/safeHref.ts New URL attribute validator with DOMPurify + SSR fallback.
packages/ui-utils/src/mergeDeep.ts Blocks __proto__/constructor/prototype keys to prevent prototype pollution.
packages/ui-utils/src/index.ts Exports safeHref and safeLinkProps publicly.
packages/ui-utils/src/tests/safeLinkProps.test.tsx Unit tests for safeLinkProps.
packages/ui-utils/src/tests/safeHref.test.tsx Unit tests for safeHref (incl. singleton isolation).
packages/ui-utils/src/tests/mergeDeep.test.tsx Adds prototype-pollution regression tests.
packages/ui-utils/package.json Adds dompurify dependency.
packages/ui-truncate-text/src/TruncateText/v2/utils/truncate.ts Replaces innerHTML string building with manual DOM node construction.
packages/ui-truncate-text/src/TruncateText/v1/utils/truncate.ts Same innerHTML removal for v1 truncation logic.
packages/ui-truncate-text/package.json Removes escape-html (+ types) dependency.
packages/ui-svg-images/src/InlineSVG/sanitizeSvg.ts Adds SVG sanitizer using a dedicated DOMPurify instance.
packages/ui-svg-images/src/InlineSVG/index.tsx Sanitizes src before parsing attributes / injecting SVG markup.
packages/ui-svg-images/src/InlineSVG/tests/sanitizeSvg.test.tsx Unit tests for SVG sanitization and singleton isolation.
packages/ui-svg-images/src/InlineSVG/tests/InlineSVG.test.tsx Component-level tests verifying sanitization is applied.
packages/ui-svg-images/package.json Adds dompurify dependency for svg-images package.
packages/ui-side-nav-bar/tsconfig.build.json Adds ui-utils TS project reference.
packages/ui-side-nav-bar/src/SideNavBar/v2/SideNavBarItem/index.tsx Uses safeLinkProps for href + rel on v2 items.
packages/ui-side-nav-bar/src/SideNavBar/v1/SideNavBarItem/index.tsx Uses safeLinkProps for href + rel on v1 items.
packages/ui-side-nav-bar/package.json Adds @instructure/ui-utils dependency.
packages/ui-navigation/src/AppNav/v2/Item/index.tsx Uses safeLinkProps for href + rel on v2 items.
packages/ui-navigation/src/AppNav/v1/Item/index.tsx Uses safeLinkProps for href + rel on v1 items.
packages/ui-menu/src/Menu/v2/MenuItem/index.tsx Uses safeLinkProps for href + rel on v2 items.
packages/ui-menu/src/Menu/v1/MenuItem/index.tsx Uses safeLinkProps for href + rel on v1 items.
packages/ui-link/src/Link/v2/index.tsx Uses safeLinkProps for href + rel in v2 Link.
packages/ui-link/src/Link/v1/index.tsx Uses safeLinkProps for href + rel in v1 Link.
packages/ui-link/src/Link/tests/Link.test.tsx Adds regression tests for href sanitization + rel defaulting.
packages/ui-file-drop/src/FileDrop/v2/index.tsx Tracks preview blob URLs and revokes them on unmount.
packages/ui-file-drop/src/FileDrop/v1/index.tsx Tracks preview blob URLs and revokes them on unmount.
packages/ui-buttons/src/BaseButton/v2/index.tsx Uses safeLinkProps for href + rel in v2 BaseButton.
packages/ui-buttons/src/BaseButton/v1/index.tsx Uses safeLinkProps for href + rel in v1 BaseButton.
packages/ui-buttons/src/BaseButton/tests/BaseButton.test.tsx Adds tests for href sanitization + rel defaulting.
packages/ui-avatar/tsconfig.build.json Adds ui-utils TS project reference.
packages/ui-avatar/src/Avatar/v1/styles.ts Sanitizes avatar src and safely escapes CSS url(...) interpolation.
packages/ui-avatar/package.json Adds @instructure/ui-utils dependency.
packages/docs/src/Nav/props.ts Simplifies query state type for safer search.
packages/docs/src/Nav/index.tsx Replaces RegExp search with lowercased substring match (ReDoS hardening).
packages/docs/src/CodeSandboxButton/index.tsx Removes xlink-based SVG usage in generated icon SVG.
packages/docs/buildScripts/samplemedia/placeholder.svg Removes xlink-based SVG usage in placeholder asset.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (1)

packages/ui-utils/src/safeHref.ts:80

  • ssrSafeHref() uses the same limited control-character stripping as extractScheme (only tab/newline/CR). If an obfuscated scheme makes it into SSR markup, users could potentially interact with the unsafe href before hydration. Normalizing the broader ASCII control-character range here reduces the risk of scheme parsing discrepancies under SSR.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +116 to +120
// Pre-check our scheme allowlist before DOMPurify, since DOMPurify's default
// ALLOWED_URI_REGEXP rejects schemes like `webcal:` that we treat as safe.
const scheme = extractScheme(value)
if (scheme && SAFE_SCHEMES.includes(scheme)) return href

Comment on lines +57 to +61
function extractScheme(value: string): string | null {
const normalized = value.replace(/[\t\n\r]/g, '').trim()
const schemeMatch = normalized.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/)
return schemeMatch ? schemeMatch[1].toLowerCase() + ':' : null
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Added a commit that fixes this

Comment on lines +51 to +57
function safeLinkProps(input: SafeLinkInput): SafeLinkOutput {
const { target, rel, tag, attr } = input
const href = safeHref(input.href, tag, attr)
const finalRel =
target === '_blank' && rel == null ? 'noopener noreferrer' : rel
return { href, target, rel: finalRel }
}
Comment on lines +87 to +95
componentWillUnmount() {
// Release any preview blob URLs we minted; otherwise the browser pins
// each File in memory until the document goes away.
this._objectUrls.forEach((url) => window.URL.revokeObjectURL(url))
this._objectUrls = []
}

_objectUrls: string[] = []

Comment on lines +88 to +96
componentWillUnmount() {
// Release any preview blob URLs we minted; otherwise the browser pins
// each File in memory until the document goes away.
this._objectUrls.forEach((url) => window.URL.revokeObjectURL(url))
this._objectUrls = []
}

_objectUrls: string[] = []

@matyasf matyasf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good! FYI I've added a commit that also filters out some weird control characters:

const normalized = value.replace(/[\u0000-\u001F\u007F]/g, '')
Character Unicode Escape
NUL U+0000 \0
SOH–US U+0001–U+001F various control chars
Tab U+0009 \t
Line Feed U+000A \n
Vertical Tab U+000B \v
Form Feed U+000C \f
Carriage Return U+000D \r
Escape U+001B \x1B
DEL U+007F \x7F

github-actions Bot pushed a commit that referenced this pull request Jul 24, 2026
@git-nandor
git-nandor merged commit 7c67546 into master Jul 24, 2026
12 checks passed
@git-nandor
git-nandor deleted the security-fix-ghsa-qqj4-cgcr-pcjj branch July 24, 2026 11:21
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.

5 participants