Skip to content

[BUG] Parser hangs indefinitely on :is() pseudo-class with comma-separated selectors #587

@kasyapArchit

Description

@kasyapArchit

Description

Juice hangs indefinitely with 100% CPU usage when processing CSS containing the :is() pseudo-class with comma-separated selectors.
The root cause is that mensch (the CSS parser used by juice) incorrectly parses these selectors, which then causes slick to hang.
While I haven't tested for other pseudo selectors but I suspect it should fail there too.

Reproduction

CodeSandbox

🔗 Live reproduction: [Codesandbox]

Code Example

const html = `<!DOCTYPE html>
<html>
<head>
  <style>
    .random-jshg5 .ShadowHTML :is(.styleUnquotedContent .unquoted-content, .styleUnquotedContent .sh-unquoted-content) {
      font-color: red !important;
    }
  </style>
</head>
<body>
  <div class="random-jshg5">
    <div class="ShadowHTML">
      <div class="styleUnquotedContent">
        <div class="unquoted-content">
          <td class="column-container">Content should have font-size: 0</td>
        </div>
      </div>
    </div>
  </div>
</body>
</html>`;

// This hangs indefinitely with 100% CPU usage
const result = juice(html);

Root Cause Analysis

The bug occurs in the following chain:

  1. Juice uses mensch to parse CSS (lib/utils.js:61)
   var parsed = mensch.parse(css, {position: true, comments: true});
  1. mensch incorrectly parses :is() selectors: mensch treats the comma inside :is(.a, .b) as a selector separator, splitting it into multiple selectors instead of keeping it as one complete selector
  2. mensch outputs malformed selector: The parsed result contains only the partial selector :is(.a (missing closing parenthesis)
  3. Juice passes this to slick: The malformed selector is then passed to slick for processing
  4. slick hangs on malformed selector: The unclosed parenthesis causes catastrophic backtracking in slick's regex, resulting in an infinite loop

When CSS contains:

:is(.styleUnquotedContent .unquoted-content, .styleUnquotedContent .sh-unquoted-content)

mensch parses this as two separate selectors:

  1. :is(.styleUnquotedContent .unquoted-content
  2. .styleUnquotedContent .sh-unquoted-content)

Instead of one complete selector.

Impact

  • Makes juice completely unusable with modern CSS
  • Causes denial of service (100% CPU, process hang)
  • Browser tab freezes (in browser environment)

The mensch parser appears to be unmaintained and lacks support for modern CSS syntax.
Migrating to a more robust engine like css or postcss would ensure better compatibility with current CSS standards and prevent parsing failures.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions