Skip to content

Sanitize profile HTML and CSS#40

Open
numbpill3d wants to merge 1 commit intomainfrom
codex/sanitize-html-and-css-using-dompurify
Open

Sanitize profile HTML and CSS#40
numbpill3d wants to merge 1 commit intomainfrom
codex/sanitize-html-and-css-using-dompurify

Conversation

@numbpill3d
Copy link
Collaborator

@numbpill3d numbpill3d commented Jun 8, 2025

Summary

  • sanitize HTML and CSS in profile editing routes using DOMPurify

Testing

  • npm test (fails: Missing Supabase env vars and Item model)

https://chatgpt.com/codex/tasks/task_e_6845129d6aa8832fa407b8d2198c3498

Summary by Sourcery

Integrate server-side sanitization for profile HTML and CSS inputs using DOMPurify (with specific tag/attribute rules) and fall back to a basic sanitizer when DOMPurify is missing.

New Features:

  • Sanitize user-provided profile HTML on the server using DOMPurify with strict whitelists
  • Sanitize user-provided profile CSS on the server using DOMPurify to strip forbidden elements
  • Provide a fallback sanitization method when the DOMPurify module is unavailable

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jun 8, 2025

Reviewer's Guide

This PR integrates DOMPurify into the profile editing routes to sanitize user-submitted HTML and CSS, establishing a jsdom-based sanitizer with a fallback and updating the POST handlers to apply sanitization before persisting.

Sequence Diagram for Profile Update with Sanitization

sequenceDiagram
    actor User
    participant Server as "Express Router (profile.js)"
    participant Purifier as "DOMPurifyLogic"
    participant UserModel as "User Model"

    User->>Server: POST /edit/html (profileHtml) OR /edit/css (profileCss)
    Server->>Purifier: sanitize(profileHtml/profileCss, options)
    Purifier-->>Server: sanitizedOutput
    Server->>UserModel: User.findById(req.user._id)
    UserModel-->>Server: user
    Server->>Server: user.profileHtml = sanitizedOutput OR user.profileCss = sanitizedOutput
    Server->>UserModel: user.save()
    UserModel-->>Server: save_success
    Server-->>User: HTTP 200 / Redirect (Profile Updated)
Loading

File-Level Changes

Change Details Files
Add DOMPurify setup with jsdom fallback
  • Import createDOMPurify from dompurify and JSDOM from jsdom in profile routes
  • Initialize DOMPurify with a jsdom window in a try block
  • Provide a simple regex-based fallback sanitize function on require failure
server/routes/profile.js
Sanitize profileHtml in POST /edit/html handler
  • Call DOMPurify.sanitize on profileHtml with specific allowed tags and attributes
  • Replace raw profileHtml assignment with the sanitized output before saving
server/routes/profile.js
Sanitize profileCss in POST /edit/css handler
  • Call DOMPurify.sanitize on profileCss with forbidden tags configuration
  • Replace raw profileCss assignment with the sanitized output before saving
server/routes/profile.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

DOMPurify = createDOMPurify(window);
} catch (err) {
console.warn('DOMPurify module not found, using fallback sanitization');
DOMPurify = { sanitize: (input) => input.replace(/</g, '&lt;').replace(/>/g, '&gt;') };
Copy link
Contributor

Choose a reason for hiding this comment

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

The fallback sanitization method used here is very basic and might not be sufficient for all cases of XSS attacks. It only replaces < and > characters, which does not cover many other potential XSS vectors such as JavaScript event handlers or style injections.

Recommendation:
Consider using a more robust sanitization library as a fallback, or ensure that dompurify is always available in your deployment environment. If neither is possible, enhance the regex to cover more XSS patterns.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @numbpill3d - I've reviewed your changes and found some issues that need to be addressed.

Blocking issues:

  • Fallback sanitization is too naive (link)
  • Enforce safe URL protocols in HTML sanitization (link)
  • Allowing inline style attribute can introduce CSS-based attacks (link)
  • DOMPurify doesn't sanitize CSS content (link)

General comments:

  • The DOMPurify configuration uses ALLOWED_ATTR and FORBID_ATTR, but the correct option names are ALLOWED_ATTRS and FORBID_ATTRS—please update those keys so attributes are properly controlled.
  • Using DOMPurify to sanitize CSS text may not be sufficient—consider integrating a dedicated CSS sanitizer or whitelisting specific CSS properties rather than relying on tag/attribute rules.
  • The fallback sanitizer in the catch block only escapes < and >, which won’t prevent XSS via attributes—either require DOMPurify or implement a more robust fallback to avoid security gaps.
Here's what I looked at during the review
  • 🟡 General issues: 2 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +7 to +15
try {
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
DOMPurify = createDOMPurify(window);
} catch (err) {
console.warn('DOMPurify module not found, using fallback sanitization');
DOMPurify = { sanitize: (input) => input.replace(/</g, '&lt;').replace(/>/g, '&gt;') };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Only fallback on missing DOMPurify dependency

Check specifically for err.code === 'MODULE_NOT_FOUND' and rethrow other errors to prevent masking unrelated issues.

Suggested change
try {
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
DOMPurify = createDOMPurify(window);
} catch (err) {
console.warn('DOMPurify module not found, using fallback sanitization');
DOMPurify = { sanitize: (input) => input.replace(/</g, '&lt;').replace(/>/g, '&gt;') };
}
try {
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
DOMPurify = createDOMPurify(window);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
console.warn('DOMPurify module not found, using fallback sanitization');
DOMPurify = { sanitize: (input) => input.replace(/</g, '&lt;').replace(/>/g, '&gt;') };
} else {
throw err;
}
}

DOMPurify = createDOMPurify(window);
} catch (err) {
console.warn('DOMPurify module not found, using fallback sanitization');
DOMPurify = { sanitize: (input) => input.replace(/</g, '&lt;').replace(/>/g, '&gt;') };
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 issue (security): Fallback sanitization is too naive

This method only escapes angle brackets and can be bypassed. For production, use a robust sanitizer or fail closed to ensure security.

Comment on lines +160 to +166
const sanitizedHtml = DOMPurify.sanitize(profileHtml, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'ul', 'ol', 'li',
'strong', 'em', 'a', 'img', 'div', 'span', 'blockquote', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'id', 'style'],
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 suggestion (security): Enforce safe URL protocols in HTML sanitization

Configure DOMPurify with an ALLOWED_URI_REGEXP (e.g., /^https?:/) to prevent unsafe protocols like javascript: in href and src attributes.

Suggested change
const sanitizedHtml = DOMPurify.sanitize(profileHtml, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'ul', 'ol', 'li',
'strong', 'em', 'a', 'img', 'div', 'span', 'blockquote', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'id', 'style'],
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
});
const sanitizedHtml = DOMPurify.sanitize(profileHtml, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'ul', 'ol', 'li',
'strong', 'em', 'a', 'img', 'div', 'span', 'blockquote', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'id', 'style'],
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
ALLOWED_URI_REGEXP: /^https?:/
});

const sanitizedHtml = DOMPurify.sanitize(profileHtml, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'ul', 'ol', 'li',
'strong', 'em', 'a', 'img', 'div', 'span', 'blockquote', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'id', 'style'],
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 issue (security): Allowing inline style attribute can introduce CSS-based attacks

Remove 'style' from ALLOWED_ATTR or ensure CSS in style attributes is properly sanitized to mitigate CSS injection risks.

try {
const { profileCss } = req.body;

const sanitizedCss = DOMPurify.sanitize(profileCss, {
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 issue (security): DOMPurify doesn't sanitize CSS content

DOMPurify does not sanitize CSS rules. Use a dedicated CSS sanitizer to allow only safe properties, remove unsafe at-rules, and block dangerous expressions like url(javascript:...).

@@ -147,10 +157,17 @@ router.get('/edit/html', ensureAuthenticated, (req, res) => {
router.post('/edit/html', ensureAuthenticated, async (req, res) => {
try {
const { profileHtml } = req.body;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Validate that profileHtml is a string before sanitizing

Add a type check to return a 400 error if profileHtml is not a string to prevent runtime issues with invalid input.

try {
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Prefer object destructuring when accessing and using properties. (use-object-destructuring)

Suggested change
const window = new JSDOM('').window;
const {window} = new JSDOM('');


ExplanationObject destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the Airbnb Javascript Style Guide

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant