Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CVE-2022-37620/ ReDoS found in htmlminifier.js #1135

Open
secdevlpr26 opened this issue Oct 27, 2022 · 17 comments
Open

CVE-2022-37620/ ReDoS found in htmlminifier.js #1135

secdevlpr26 opened this issue Oct 27, 2022 · 17 comments

Comments

@secdevlpr26
Copy link

A Regular Expression Denial of Service (ReDoS) flaw was found in kangax html-minifier 4.0.0 via the candidate variable in htmlminifier.js.
The ReDoS vulnerability can be mitigated with several best practices described here: [https://snyk.io/blog/redos-and-catastrophic-backtracking/]

@michaeljauk
Copy link

Is anyone willing to work on this?

@blv-raulcatalan
Copy link

Any update on this?

@rquadling
Copy link

rquadling commented Mar 22, 2023

One of the lines referred to be the CVE has the following regex \s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$

Would changing it to \s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+|)x)$ be enough?

\s+([1-9][0-9]*w|[0-9]+(?=(\.[0-9]+|))\2x)$ is another way that should stop the backtracking (based this upon https://snyk.io/blog/redos-and-catastrophic-backtracking/.

@timbomckay
Copy link

timbomckay commented Apr 10, 2023

I stumbled upon a fork of this maintained by terser: https://www.npmjs.com/package/html-minifier-terser

Probably the way to go.

@littleblack111
Copy link

any updates?

@timbomckay
Copy link

any updates?

@littleblack111 as mentioned above, along with the slew of references to others switching, html-minifier-terser is probably the best solution. Aside from some repo adjustments this solution appears to be fairly abandoned, especially since the package hasn't been updated in 5 years, while html-minified-terser is maintained by Terser.

@rquadling
Copy link

Now I've just got to remember what project I'm involved in that has this one as an issue!

@Hessah95
Copy link

any update on this issue?

@timbomckay
Copy link

any update on this issue?

@Hessah95 The comments in this thread point to another solution that's maintained.

@ra-dave
Copy link

ra-dave commented Oct 6, 2024

html-minifier-terser latest version (7.2.0) still has the vulnerability? I just installed it and it says so.

@marcoandre1
Copy link

@ra-dave I first installed version 7.2.0 as well, but it broke my unit tests. I then went for version 5.1.1 which keeps my unit tests passing and it made the vulnerability go away. Of course, this only works if you don’t need the newest version.

@DanielRuf
Copy link

DanielRuf commented Nov 2, 2024

Can anyone provide a working proof of concept that confirms the issue can be actively exploited?

Also what would be the definitive solution here?

@DanielRuf
Copy link

DanielRuf commented Nov 3, 2024

It seems the CVE record differs from https://security.snyk.io/vuln/SNYK-JS-HTMLMINIFIER-3091181, which contains more details.

Not sure why this is the case. I will clarify with Snyk.

Edit: the candidate variable is not even used here, because the relevant code branch for srcset (else if (isSrcset(attrName, tag)) {) is not even executed in the PoC.

It seems the CVE description is more than misleading, at best incomplete and inaccurate.

@DanielRuf
Copy link

DanielRuf commented Nov 3, 2024

It seems the PoC is more complex than needed.

The code basically makes not much sense:
minify(attrName = '\t'.repeat(547703) + '.\t1x', tag = '\t'.repeat(547703) + '.\t1x' )

It should be probably this:

minify('\t'.repeat(547703) + '.\t1x', '\t'.repeat(547703) + '.\t1x' )

Because the minify function has the following signature:
exports.minify = function(value, options) {

The options parameter does not have an impact. Because when I leave the second parameter undefined, then time node test.js gives the same duration.

So the following PoC code is sufficient:
result = minify('\t'.repeat(54770) + '.\t1x' )

The issue is in the following code part:

  var customFragments = options.ignoreCustomFragments.map(function(re) {
    return re.source;
  });
  if (customFragments.length) {
    var reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g');
    // temporarily replace custom ignored fragments with unique attributes
    value = value.replace(reCustomIgnore, function(match) {
      if (!uidAttr) {
        uidAttr = uniqueId(value);
        uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
        if (options.minifyCSS) {
          options.minifyCSS = (function(fn) {
            return function(text, type) {
              text = text.replace(uidPattern, function(match, prefix, index) {
                var chunks = ignoredCustomMarkupChunks[+index];
                return chunks[1] + uidAttr + index + uidAttr + chunks[2];
              });
              var ids = [];
              new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function(warning) {
                var match = uidPattern.exec(warning);
                if (match) {
                  var id = uidAttr + match[2] + uidAttr;
                  text = text.replace(id, ignoreCSS(id));
                  ids.push(id);
                }
              });
              text = fn(text, type);
              ids.forEach(function(id) {
                text = text.replace(ignoreCSS(id), id);
              });
              return text;
            };
          })(options.minifyCSS);
        }
        if (options.minifyJS) {
          options.minifyJS = (function(fn) {
            return function(text, type) {
              return fn(text.replace(uidPattern, function(match, prefix, index) {
                var chunks = ignoredCustomMarkupChunks[+index];
                return chunks[1] + uidAttr + index + uidAttr + chunks[2];
              }), type);
            };
          })(options.minifyJS);
        }
      }
      var token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
      ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
      return '\t' + token + '\t';
    });
  }

After checking further, the following line is the problem:

var reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g');
which results in the following by default:
var reCustomIgnore = new RegExp('\\s*(?:<%[\\s\\S]*?%>|<\\?[\\s\\S]*?\\?>)+\\s*', 'g');

The person whou found the problem, probably used https://devina.io/redos-checker, which also uses the same type of attack string:

image

The problem
usage of * and + in reCustomIgnore without any upper bounds ({0,50}, {1,50}), which leads to catastrophic backtracking.

Migitations

  • set options.ignoreCustomFragments = []; - this may break some minifications
  • set a global timeout in your script, which should be done in general in production (applies to any programming language to prevent the consumption of too many resources and to mitigate DoS), see for example https://www.nearform.com/insights/using-abortsignal-in-node-js/

@DanielRuf
Copy link

For anyone asking, html-minifier-terser is also affected. So switching to it will not resolve this. You will just get less reported CVEs but you will also have a false-negative then.

@sidewayss
Copy link

And of course it affects all the other downstream packages, including these:

  • gulp-htmlmin
  • gulp-html-minifier-terser
  • gulp-htmlclean

@ananzh
Copy link

ananzh commented Dec 4, 2024

Hi any updates on the plan to fix this?

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

No branches or pull requests