Skip to content

Sanitize owner-supplied HTML before rendering - #57

Open
paynejd wants to merge 8 commits into
masterfrom
OpenConceptLab/ocl_online#557-sanitize-html
Open

paynejd wants to merge 8 commits into
masterfrom
OpenConceptLab/ocl_online#557-sanitize-html

Conversation

@paynejd

@paynejd paynejd commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Hardening: HTML from the API, from org and repository owners, from client config, or from a configured URL is now sanitized before TBv2 renders it, using DOMPurify 3.4.16.

SafeHTML (src/components/common/SafeHTML.jsx) holds one conservative allowlist.

  • Tags: formatting, links, lists, tables and images. Scripts, frames, embeds, forms and event-handler attributes are removed.
  • URLs: links keep only http, https, mailto, tel and relative URLs, and get rel="noopener noreferrer". Images may also use raster data: images (png, jpeg, gif, webp).
  • Inline styles are limited to formatting properties. Positioning, viewport-sized and negative sizes, and url() values are removed.
  • Classes: only the editor's formatting classes (ql-align-*, ql-indent-*, ql-direction-*, ql-size-*, ql-font-*) are kept.
  • Ids are prefixed (user-content-).
  • The wrapper: content renders in a paint-contained wrapper that scrolls horizontally when content is wider than its box.
  • Links: the href="/ to href="/#/ rewrite is unchanged.

Other changes:

  • Notifications (Alertify success, error, warning, message, notify) go through a stricter allowlist: basic formatting and links, with no classes, styles or ids.
  • Dialog text and the hierarchy tooltip escape the dynamic values they interpolate, using lodash escape.
  • The rich-text editor sanitizes what it's given and what it saves.
  • The client-config JSON editor is now a plain text area. It replaces react-json-editor-ajrm, which rendered config through HTML.
  • Window config values (API_URL, OIDC settings and the rest) are only used when they're strings.

No CSP in this PR.

Places changed

  • common/SafeHTML.jsx (new) and common/windowConfig.js (new)
  • orgs/Overview.jsx, common/About.jsx, common/CustomMarkup.jsx (via CustomText.jsx), orgs/HomeHeader.jsx, common/ErrorUI.jsx, search/ResultsTable.jsx (the search-highlight tooltip and the FHIR copyright)
  • common/RTEditor.jsx, common/JSONEditor.jsx, concepts/ConceptHierarchyTree.jsx
  • dialog text in Task.jsx, ViewConfigForm.jsx, RepoExternalExports.jsx, ConceptContainerVersionList.jsx, collections/Versions.jsx, UserManagement.jsx, MembersForm.jsx and Login.jsx
  • src/index.jsx (the notification wrapper), common/utils.js and common/ssoSession.js (window config)

Markdown rendering (CustomMarkdown, ChangelogMarkdown) uses react-markdown 6 without a raw-HTML plugin, so it doesn't render embedded HTML. It is left as it is.

Testing

  • A throwaway jsdom and DOMPurify check (not committed) against the real sanitize functions: 35 cases pass, covering what's removed and what's kept.
  • ESLint: eslint --ext .jsx,.js src/ is clean.
  • Build: npm run build compiles the JS. The full build fails here only on Sass modules, because node-sass doesn't build in this environment. Master wasn't built for comparison, so CI or a deploy build needs to confirm it.
  • Lock file: package-lock.json stays at lockfileVersion 1. The only changes add DOMPurify and remove react-json-editor-ajrm, and npm ci passes.
  • To check in a browser before release: About pages, org headers, the concept hierarchy tooltip, notifications, the rich-text editor and the client-config drawer. Owner content that relied on its own classes, opacity, float or background shorthand, or that embedded video, will render without them.

Related: OpenConceptLab/ocl_online#557

🤖 Generated with Claude Code

https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB

…endering

Hardening: HTML supplied by org and repository owners, client config and
search results is now sanitized with DOMPurify (3.4.16) before it is
rendered, through a new SafeHTML component with a conservative allowlist
of formatting, link, list, table and image markup. Links get
rel="noopener noreferrer"; only http, https, mailto and relative URLs
are kept. Existing wrappers, classes and styles are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
@paynejd

paynejd commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Review, pass 1 (Codex, commit 215f97f): four findings, all being fixed here.

  • Two more places render HTML without sanitizing it: a tooltip, and dialog and notification text.
  • Inline styles need tighter limits.
  • Element ids in sanitized content should be namespaced.

…gs; limit inline styles; namespace ids

Hardening follow-up: escape API values interpolated into tooltip and
dialog HTML, sanitize notifier messages, keep only presentational CSS
properties in inline styles and contain the rendered box, prefix ids and
names from rendered HTML, and ignore non-string window config values.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
@paynejd

paynejd commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Review, pass 2 (Codex, commit 3a65d4e): two findings, both being fixed. The rich-text editor's input needs sanitizing too, and notification messages need their own stricter allowlist.

paynejd and others added 2 commits October 21, 2026 14:35
…ication allowlist; owner HTML keeps no classes

Hardening follow-up: sanitize values before they reach the rich text
editor, use a separate minimal allowlist for notification text (no
class, style or id), drop class attributes from owner-supplied HTML, and
reject viewport-relative, computed and negative sizes in inline styles.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
…on display

Hardening follow-up: owner-supplied HTML keeps only the rich text
editor's formatting classes (align, indent, direction, size, font) and
drops every other class. The editor keeps its own output in state and
only the value passed up is sanitized.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
@paynejd

paynejd commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Review, pass 3 (Codex, commit 5444592): one finding. Wide content such as tables is clipped instead of scrolling. It's being fixed by giving the wrapper horizontal scrolling.

paynejd and others added 2 commits October 21, 2026 14:43
…ally when it's wider than its box

Hardening follow-up: the sanitized-content wrapper keeps paint
containment and now sets overflow-x: auto so wide tables and images stay
reachable. Owner CSS still cannot set overflow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
…aller helpers (simplify pass)

Use lodash's escape instead of a hand-rolled helper, drop the editor
sanitize alias and the hash-link helper, un-export constants nothing
imports, and drop an unneeded guard. No behaviour change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
@paynejd

paynejd commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Checks (head 4a18e92): the scroll fix and the simplification cuts each passed a clean Codex check. An adversarial check follows.

paynejd and others added 2 commits October 21, 2026 15:16
…indow config; logical CSS sizes; allow tel: and raster data images

Hardening follow-up: the configuration editor is now a plain text area
instead of a library that renders HTML (dependency removed), SSO window
settings ignore non-string values, the inline-style size checks cover
logical and min- variants, and links may use tel: while data: URLs are
kept only for raster images.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
… object

Hardening follow-up: the plain JSON editor reports null, scalars and
arrays as errors instead of applying them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVCCrTucXFfAwXL8jNgbYB
@paynejd

paynejd commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Review loop complete (head 33b4856). Codex passes 1–3 and every follow-up check are clean, the adversarial check's coverage gaps are fixed, and CI is green. Before release, check the affected pages listed in the description in a browser. Ready to merge.

@paynejd
paynejd requested a review from snyaggarwal October 21, 2026 20:46

This branch has not been deployed

No deployments
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