html-minifier-next 1.0.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # HTML Minifier Next (HTMLMinifier)
2
2
 
3
- [![NPM version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next)
3
+ [![npm version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next)
4
4
  <!-- [![Build Status](https://github.com/j9t/html-minifier-next/workflows/CI/badge.svg)](https://github.com/j9t/html-minifier-next/actions?workflow=CI) -->
5
5
 
6
6
  (This project is based on [Terser’s html-minifier-terser](https://github.com/terser/html-minifier-terser), which in turn is based on [Juriy Zaytsev’s html-minifier](https://github.com/kangax/html-minifier). It was set up because as of May 2025, both html-minifier-terser and html-minifier seem unmaintained. **This project is currently under test.** If it seems maintainable to me, [Jens](https://meiert.com/), even without community support, the project will be updated and documented further. The following documentation largely matches the original project.)
@@ -54,23 +54,23 @@ How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Pe
54
54
 
55
55
  | Site | Original size (KB) | HTMLMinifier | minimize | htmlcompressor.com |
56
56
  | --- | --- | --- | --- | --- |
57
- | [Amazon](https://www.amazon.com/) | 6 | **4** | 4 | n/a |
58
- | [BBC](https://www.bbc.co.uk/) | 759 | **699** | 752 | n/a |
59
- | [ECMAScript](https://tc39.es/ecma262/) | 7195 | **6352** | 6571 | n/a |
57
+ | [Amazon](https://www.amazon.com/) | 695 | **625** | 682 | n/a |
58
+ | [BBC](https://www.bbc.co.uk/) | 655 | **602** | 649 | n/a |
59
+ | [ECMAScript](https://tc39.es/ecma262/) | 7197 | **6353** | 6573 | n/a |
60
60
  | [EFF](https://www.eff.org/) | 60 | **51** | 54 | n/a |
61
61
  | [Eloquent JavaScript](https://eloquentjavascript.net/) | 6 | **5** | 6 | n/a |
62
- | [FAZ](https://www.faz.net/aktuell/) | 1738 | **1614** | 1649 | n/a |
63
- | [Frontend Dogma](https://frontenddogma.com/) | 119 | **115** | 128 | n/a |
64
- | [Google](https://www.google.com/) | 50 | **46** | 50 | n/a |
65
- | [HTMLMinifier](https://github.com/kangax/html-minifier) | 363 | **243** | 341 | n/a |
66
- | [Mastodon](https://mastodon.social/explore) | 22 | **13** | 21 | n/a |
67
- | [NBC](https://www.nbc.com/) | 1204 | **1099** | 1191 | n/a |
68
- | [New York Times](https://www.nytimes.com/) | 900 | **760** | 890 | n/a |
69
- | [United Nations](https://www.un.org/) | 10 | **7** | 8 | n/a |
62
+ | [FAZ](https://www.faz.net/aktuell/) | 1793 | **1667** | 1705 | n/a |
63
+ | [Frontend Dogma](https://frontenddogma.com/) | 116 | **112** | 125 | n/a |
64
+ | [Google](https://www.google.com/) | 51 | **46** | 51 | n/a |
65
+ | [HTMLMinifier](https://github.com/kangax/html-minifier) | 366 | **245** | 343 | n/a |
66
+ | [Mastodon](https://mastodon.social/explore) | 37 | **27** | 36 | n/a |
67
+ | [NBC](https://www.nbc.com/) | 1022 | **932** | 1010 | n/a |
68
+ | [New York Times](https://www.nytimes.com/) | 951 | **809** | 939 | n/a |
69
+ | [United Nations](https://www.un.org/) | 9 | **7** | 8 | n/a |
70
70
  | [W3C](https://www.w3.org/) | 51 | **36** | 42 | n/a |
71
71
  | [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | 114 | **100** | 107 | n/a |
72
72
 
73
- ## Options Quick Reference
73
+ ## Options quick reference
74
74
 
75
75
  Most of the options are disabled by default.
76
76
 
@@ -78,6 +78,7 @@ Most of the options are disabled by default.
78
78
  | --- | --- | --- |
79
79
  | `caseSensitive` | Treat attributes in case sensitive manner (useful for custom HTML tags) | `false` |
80
80
  | `collapseBooleanAttributes` | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier#collapse_boolean_attributes) | `false` |
81
+ | `customFragmentQuantifierLimit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
81
82
  | `collapseInlineTagWhitespace` | Don’t leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` |
82
83
  | `collapseWhitespace` | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` |
83
84
  | `conservativeCollapse` | Always collapse to 1 space (never remove it entirely). Must be used in conjunction with `collapseWhitespace=true` | `false` |
@@ -92,6 +93,7 @@ Most of the options are disabled by default.
92
93
  | `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
93
94
  | `includeAutoGeneratedTags` | Insert tags generated by HTML parser | `true` |
94
95
  | `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` |
96
+ | `maxInputLength` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
95
97
  | `maxLineLength` | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points |
96
98
  | `minifyCSS` | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
97
99
  | `minifyJS` | Minify JavaScript in script elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
@@ -154,6 +156,63 @@ Output of resulting markup (e.g. `<p>foo</p>`)
154
156
 
155
157
  HTMLMinifier can’t know that original markup was only half of the tree; it does its best to try to parse it as a full tree and it loses information about tree being malformed or partial in the beginning. As a result, it can’t create a partial/malformed tree at the time of the output.
156
158
 
159
+ ## Security
160
+
161
+ ### ReDoS protection
162
+
163
+ This minifier includes protection against regular expression denial of service (ReDoS) attacks:
164
+
165
+ * Custom fragment quantifier limits: The `customFragmentQuantifierLimit` option (default: 200) prevents exponential backtracking by replacing unlimited quantifiers (`*`, `+`) with bounded ones in regular expressions.
166
+
167
+ * Input length limits: The `maxInputLength` option allows you to set a maximum input size to prevent processing of excessively large inputs that could cause performance issues.
168
+
169
+ * Enhanced pattern detection: The minifier detects and warns about various ReDoS-prone patterns including nested quantifiers, alternation with quantifiers, and multiple unlimited quantifiers.
170
+
171
+ **Important:** When using custom `ignoreCustomFragments`, ensure your regular expressions don’t contain unlimited quantifiers (`*`, `+`) without bounds, as these can lead to ReDoS vulnerabilities.
172
+
173
+ (Further improvements are needed. Contributions welcome.)
174
+
175
+ #### Custom fragment examples
176
+
177
+ **Safe patterns** (recommended):
178
+
179
+ ```javascript
180
+ ignoreCustomFragments: [
181
+ /<%[\s\S]{0,1000}?%>/, // JSP/ASP with explicit bounds
182
+ /<\?php[\s\S]{0,5000}?\?>/, // PHP with bounds
183
+ /\{\{[^}]{0,500}\}\}/ // Handlebars without nested braces
184
+ ]
185
+ ```
186
+
187
+ **Potentially unsafe patterns** (will trigger warnings):
188
+
189
+ ```javascript
190
+ ignoreCustomFragments: [
191
+ /<%[\s\S]*?%>/, // Unlimited quantifiers
192
+ /<!--[\s\S]*?-->/, // Could cause issues with very long comments
193
+ /\{\{.*?\}\}/, // Nested unlimited quantifiers
194
+ /(script|style)[\s\S]*?/ // Multiple unlimited quantifiers
195
+ ]
196
+ ```
197
+
198
+ **Template engine configurations:**
199
+
200
+ ```javascript
201
+ // Handlebars/Mustache
202
+ ignoreCustomFragments: [/\{\{[\s\S]{0,1000}?\}\}/]
203
+
204
+ // Liquid (Jekyll)
205
+ ignoreCustomFragments: [/\{%[\s\S]{0,500}?%\}/, /\{\{[\s\S]{0,500}?\}\}/]
206
+
207
+ // Angular
208
+ ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
209
+
210
+ // Vue.js
211
+ ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
212
+ ```
213
+
214
+ **Important:** When using custom `ignoreCustomFragments`, the minifier automatically applies bounded quantifiers to prevent ReDoS attacks, but you can also write safer patterns yourself using explicit bounds.
215
+
157
216
  ## Running benchmarks
158
217
 
159
218
  Benchmarks for minified HTML:
@@ -168,4 +227,8 @@ npm run benchmark
168
227
 
169
228
  ```shell
170
229
  npm run serve
171
- ```
230
+ ```
231
+
232
+ ## Acknowledgements
233
+
234
+ With many thanks to all the previous authors of HTML Minifier, especially [Juriy Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, like [Daniel Ruf](https://github.com/DanielRuf).
package/cli.js CHANGED
@@ -101,6 +101,7 @@ function parseString(value) {
101
101
  const mainOptions = {
102
102
  caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)',
103
103
  collapseBooleanAttributes: 'Omit attribute values from boolean attributes',
104
+ customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks (default: 200)', parseInt],
104
105
  collapseInlineTagWhitespace: 'Collapse white space around inline tag',
105
106
  collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.',
106
107
  conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)',
@@ -115,6 +116,7 @@ const mainOptions = {
115
116
  ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. <?php ... ?>, {{ ... }})', parseJSONRegExpArray],
116
117
  includeAutoGeneratedTags: 'Insert tags generated by HTML parser',
117
118
  keepClosingSlash: 'Keep the trailing slash on singleton elements',
119
+ maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseInt],
118
120
  maxLineLength: ['Max line length', parseInt],
119
121
  minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],
120
122
  minifyJS: ['Minify Javascript in script elements and on* attributes', parseJSON],
@@ -304,4 +306,4 @@ if (inputDir || outputDir) {
304
306
  process.stdin.on('data', function (data) {
305
307
  content += data;
306
308
  }).on('end', writeMinify);
307
- }
309
+ }
@@ -1333,6 +1333,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr) {
1333
1333
  }
1334
1334
 
1335
1335
  async function minifyHTML(value, options, partialMarkup) {
1336
+ // Check input length limitation to prevent ReDoS attacks
1337
+ if (options.maxInputLength && value.length > options.maxInputLength) {
1338
+ throw new Error(`Input length (${value.length}) exceeds maximum allowed length (${options.maxInputLength})`);
1339
+ }
1340
+
1336
1341
  if (options.collapseWhitespace) {
1337
1342
  value = collapseWhitespace(value, options, true, true);
1338
1343
  }
@@ -1377,8 +1382,24 @@ async function minifyHTML(value, options, partialMarkup) {
1377
1382
  return re.source;
1378
1383
  });
1379
1384
  if (customFragments.length) {
1380
- const reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g');
1381
- // temporarily replace custom ignored fragments with unique attributes
1385
+ // Warn about potential ReDoS if custom fragments use unlimited quantifiers
1386
+ for (let i = 0; i < customFragments.length; i++) {
1387
+ if (/[*+]/.test(customFragments[i])) {
1388
+ options.log('Warning: Custom fragment contains unlimited quantifiers (* or +) which may cause ReDoS vulnerability');
1389
+ break;
1390
+ }
1391
+ }
1392
+
1393
+ // Safe approach: Use bounded quantifiers instead of unlimited ones to prevent ReDoS
1394
+ const maxQuantifier = options.customFragmentQuantifierLimit || 200;
1395
+ const whitespacePattern = `\\s{0,${maxQuantifier}}`;
1396
+
1397
+ // Use bounded quantifiers to prevent ReDoS - this approach prevents exponential backtracking
1398
+ const reCustomIgnore = new RegExp(
1399
+ whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern,
1400
+ 'g'
1401
+ );
1402
+ // Temporarily replace custom ignored fragments with unique attributes
1382
1403
  value = value.replace(reCustomIgnore, function (match) {
1383
1404
  if (!uidAttr) {
1384
1405
  uidAttr = uniqueId(value);