From 9e37e948cabcee0f6da752e0def0c7a246e1d6d4 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sun, 1 Jun 2025 16:36:56 +0200 Subject: [PATCH 01/13] fix: add missing newline at end of file Ensures proper formatting by adding a newline at the end of the file. This change helps maintain consistency and adheres to coding standards. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.js b/cli.js index d085c8a2..8be3cf7a 100755 --- a/cli.js +++ b/cli.js @@ -304,4 +304,4 @@ if (inputDir || outputDir) { process.stdin.on('data', function (data) { content += data; }).on('end', writeMinify); -} \ No newline at end of file +} From 9c56bf8354cc6c874e89934cd82ef727dd2a3ff1 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sun, 1 Jun 2025 16:37:27 +0200 Subject: [PATCH 02/13] fix: refine regex for custom fragments handling Improve the regex logic to handle custom fragments more robustly by accounting for nested occurrences. This ensures better accuracy when temporarily replacing ignored fragments. [References: https://github.com/kangax/html-minifier/issues/1135] (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- src/htmlminifier.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 6c2d6ddf..6464821e 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -888,7 +888,7 @@ async function minifyHTML(value, options, partialMarkup) { return re.source; }); if (customFragments.length) { - const reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g'); + const reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')(?:(?:(?!\\s*(?:' + customFragments.join('|') + '))\\s)*(?:' + customFragments.join('|') + '))*\\s*', 'g'); // temporarily replace custom ignored fragments with unique attributes value = value.replace(reCustomIgnore, function (match) { if (!uidAttr) { From 82e14dec275c7dddec580140bbf3529350fa9601 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sun, 1 Jun 2025 16:37:39 +0200 Subject: [PATCH 03/13] chore: bump version to 1.0.2 Update package.json and package-lock.json to reflect the version change from 1.0.1 to 1.0.2. This ensures consistency across the project metadata. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 291d3012..8f719fd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "html-minifier-next", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "html-minifier-next", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "change-case": "^4.1.2", diff --git a/package.json b/package.json index 356d0cf4..6d9084a7 100644 --- a/package.json +++ b/package.json @@ -90,5 +90,5 @@ "test:web": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --verbose --environment=jsdom" }, "type": "module", - "version": "1.0.1" + "version": "1.0.2" } \ No newline at end of file From bf322651ed6a54a7b37073eac04b83df964177ef Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sun, 1 Jun 2025 17:09:48 +0200 Subject: [PATCH 04/13] fix: prevent ReDoS in custom fragments processing Introduce input length validation to mitigate ReDoS risks in the custom fragments regex. Long inputs now fall back to a simpler, safer pattern. Added tests to ensure performance and correctness under various edge cases. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- src/htmlminifier.js | 17 +++++++++++--- tests/minifier.spec.js | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 6464821e..d804908e 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -888,8 +888,19 @@ async function minifyHTML(value, options, partialMarkup) { return re.source; }); if (customFragments.length) { - const reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')(?:(?:(?!\\s*(?:' + customFragments.join('|') + '))\\s)*(?:' + customFragments.join('|') + '))*\\s*', 'g'); - // temporarily replace custom ignored fragments with unique attributes + // Safe approach: Use original pattern but with input length validation to prevent ReDoS + // If input is too long, fall back to simpler pattern that handles fragments individually + const maxSafeLength = 50000; // Reasonable limit for the complex pattern + + let reCustomIgnore; + if (value.length > maxSafeLength) { + // For very long inputs, use simple individual fragment matching to prevent ReDoS + reCustomIgnore = new RegExp('(\\s*)(' + customFragments.join('|') + ')(\\s*)', 'g'); + } else { + // For normal inputs, use the original pattern (it's only vulnerable on very long inputs) + reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')(?:(?:(?!\\s*(?:' + customFragments.join('|') + '))\\s)*(?:' + customFragments.join('|') + '))*\\s*', 'g'); + } + // Temporarily replace custom ignored fragments with unique attributes value = value.replace(reCustomIgnore, function (match) { if (!uidAttr) { uidAttr = uniqueId(value); @@ -1363,4 +1374,4 @@ export const minify = async function (value, options) { return result; }; -export default { minify }; +export default { minify }; \ No newline at end of file diff --git a/tests/minifier.spec.js b/tests/minifier.spec.js index 067ef273..9e389360 100644 --- a/tests/minifier.spec.js +++ b/tests/minifier.spec.js @@ -3594,3 +3594,54 @@ test('minify Content-Security-Policy', async () => { input = ''; expect(await minify(input)).toBe(input); }); + +test('ReDoS prevention in custom fragments processing', async () => { + // Test long sequences of whitespace that could trigger ReDoS + const longWhitespace = ' '.repeat(10000); + const phpFragments = [/<%[\s\S]*?%>/g, /<\?[\s\S]*?\?>/g]; + + // Test case 1: Long whitespace before custom fragment + const input1 = `
${longWhitespace}
`; + const startTime1 = Date.now(); + const result1 = await minify(input1, { + ignoreCustomFragments: phpFragments, + collapseWhitespace: true + }); + const endTime1 = Date.now(); + + // Should complete quickly (under 1 second) + expect(endTime1 - startTime1).toBeLessThan(1000); + expect(result1).toContain(''); + + // Test case 2: Multiple consecutive fragments with long whitespace + const input2 = `
${longWhitespace}${longWhitespace}${longWhitespace}
`; + const startTime2 = Date.now(); + const result2 = await minify(input2, { + ignoreCustomFragments: phpFragments, + collapseWhitespace: true + }); + const endTime2 = Date.now(); + + // Should complete quickly (under 1 second) + expect(endTime2 - startTime2).toBeLessThan(1000); + expect(result2).toContain(''); + expect(result2).toContain(''); + + // Test case 3: Back-to-back fragments with varying whitespace + const backToBackFragments = Array(100).fill(0).map((_, i) => + `${' '.repeat(i % 50)}` + ).join(''); + const input3 = `
${backToBackFragments}
`; + + const startTime3 = Date.now(); + const result3 = await minify(input3, { + ignoreCustomFragments: phpFragments, + collapseWhitespace: true + }); + const endTime3 = Date.now(); + + // Should complete quickly (under 2 seconds for 100 fragments) + expect(endTime3 - startTime3).toBeLessThan(2000); + expect(result3).toContain(''); + expect(result3).toContain(''); +}); From 45d571adfd6cd802036571a669aff1f690ddec82 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sun, 8 Jun 2025 20:04:33 +0200 Subject: [PATCH 05/13] fix: simplify regex for custom fragment matching Simplified the regular expression used for normal inputs to improve efficiency and maintainability. The updated regex is less prone to potential issues while retaining the necessary functionality. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- src/htmlminifier.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index d804908e..b0fd3394 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -897,8 +897,8 @@ async function minifyHTML(value, options, partialMarkup) { // For very long inputs, use simple individual fragment matching to prevent ReDoS reCustomIgnore = new RegExp('(\\s*)(' + customFragments.join('|') + ')(\\s*)', 'g'); } else { - // For normal inputs, use the original pattern (it's only vulnerable on very long inputs) - reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')(?:(?:(?!\\s*(?:' + customFragments.join('|') + '))\\s)*(?:' + customFragments.join('|') + '))*\\s*', 'g'); + // For normal inputs, use the original pattern (it’s only vulnerable on very long inputs) + reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g'); } // Temporarily replace custom ignored fragments with unique attributes value = value.replace(reCustomIgnore, function (match) { From ea41da35a9f811f51b9d0c2ad902c9e8660ec254 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 14 Jun 2025 14:02:34 +0200 Subject: [PATCH 06/13] fix(security): mitigate ReDoS in custom fragments, add maxInputLength, improve docs and tests\n\n- Use bounded quantifiers in default ignoreCustomFragments\n- Add maxInputLength option and warnings\n- Warn on unsafe custom regexes\n- Update docs and CLI\n- Add/extend tests for ReDoS prevention --- src/htmlminifier.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index b0fd3394..6b4d0578 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -1374,4 +1374,4 @@ export const minify = async function (value, options) { return result; }; -export default { minify }; \ No newline at end of file +export default { minify }; From 9e3e6dea1894123cffe51e26b9a324bc91063b2d Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 14 Jun 2025 14:56:53 +0200 Subject: [PATCH 07/13] chore: bump version to 1.1.0 Update package.json and package-lock.json to reflect the new version 1.1.0. This prepares for the release with any included changes. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f719fd5..88cbc325 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "html-minifier-next", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "html-minifier-next", - "version": "1.0.2", + "version": "1.1.0", "license": "MIT", "dependencies": { "change-case": "^4.1.2", diff --git a/package.json b/package.json index 6d9084a7..2b4a4dd1 100644 --- a/package.json +++ b/package.json @@ -90,5 +90,5 @@ "test:web": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --verbose --environment=jsdom" }, "type": "module", - "version": "1.0.2" + "version": "1.1.0" } \ No newline at end of file From 147599321dc08d054095afed3ed7a5e1df5647d1 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 14 Jun 2025 14:57:05 +0200 Subject: [PATCH 08/13] fix: mitigate ReDoS vulnerability in custom fragment handling Add input length validation and bounded quantifiers to prevent ReDoS vulnerabilities in HTML minification. Warn users about potential risks of using unlimited quantifiers in custom fragments. This improves the security and stability of the minification process. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- src/htmlminifier.js | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 6b4d0578..97b4683b 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -844,6 +844,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr) { } async function minifyHTML(value, options, partialMarkup) { + // Check input length limitation to prevent ReDoS attacks + if (options.maxInputLength && value.length > options.maxInputLength) { + throw new Error(`Input length (${value.length}) exceeds maximum allowed length (${options.maxInputLength})`); + } + if (options.collapseWhitespace) { value = collapseWhitespace(value, options, true, true); } @@ -888,18 +893,23 @@ async function minifyHTML(value, options, partialMarkup) { return re.source; }); if (customFragments.length) { - // Safe approach: Use original pattern but with input length validation to prevent ReDoS - // If input is too long, fall back to simpler pattern that handles fragments individually - const maxSafeLength = 50000; // Reasonable limit for the complex pattern - - let reCustomIgnore; - if (value.length > maxSafeLength) { - // For very long inputs, use simple individual fragment matching to prevent ReDoS - reCustomIgnore = new RegExp('(\\s*)(' + customFragments.join('|') + ')(\\s*)', 'g'); - } else { - // For normal inputs, use the original pattern (it’s only vulnerable on very long inputs) - reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g'); + // Warn about potential ReDoS if custom fragments use unlimited quantifiers + for (let i = 0; i < customFragments.length; i++) { + if (/[*+]/.test(customFragments[i])) { + options.log('Warning: Custom fragment contains unlimited quantifiers (* or +) which may cause ReDoS vulnerability'); + break; + } } + + // Safe approach: Use bounded quantifiers instead of unlimited ones to prevent ReDoS + const maxQuantifier = options.customFragmentQuantifierLimit || 1000; + const whitespacePattern = `\\s{0,${maxQuantifier}}`; + + // Use bounded quantifiers to prevent ReDoS - this approach prevents exponential backtracking + const reCustomIgnore = new RegExp( + whitespacePattern + '(?:' + customFragments.join('|') + '){1,' + maxQuantifier + '}' + whitespacePattern, + 'g' + ); // Temporarily replace custom ignored fragments with unique attributes value = value.replace(reCustomIgnore, function (match) { if (!uidAttr) { From 5d1c1275354674baae8328c2171c7763a05e2847 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 14 Jun 2025 14:57:18 +0200 Subject: [PATCH 09/13] feat: add options to prevent ReDoS attacks Introduce `customFragmentQuantifierLimit` and `maxInputLength` to improve security by mitigating ReDoS attack risks. These options provide limits for regex quantifiers and input length. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- cli.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli.js b/cli.js index 8be3cf7a..71c7d3a4 100755 --- a/cli.js +++ b/cli.js @@ -101,6 +101,7 @@ function parseString(value) { const mainOptions = { caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)', collapseBooleanAttributes: 'Omit attribute values from boolean attributes', + customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks', parseInt], collapseInlineTagWhitespace: 'Collapse white space around inline tag', collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.', conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)', @@ -115,6 +116,7 @@ const mainOptions = { ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. , {{ ... }})', parseJSONRegExpArray], includeAutoGeneratedTags: 'Insert tags generated by HTML parser', keepClosingSlash: 'Keep the trailing slash on singleton elements', + maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseInt], maxLineLength: ['Max line length', parseInt], minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON], minifyJS: ['Minify Javascript in script elements and on* attributes', parseJSON], From 072b9b6fa94f05f0046e5b11299c3dc507fefcdf Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 14 Jun 2025 14:57:29 +0200 Subject: [PATCH 10/13] docs: add security details and new options to README Updated the README to include new security-related options (`customFragmentQuantifierLimit` and `maxInputLength`) and detailed explanations on ReDoS protection. Added a dedicated "Security" section for clarity and user guidance. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index ac9de08e..b9062dd8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Most of the options are disabled by default. | --- | --- | --- | | `caseSensitive` | Treat attributes in case sensitive manner (useful for custom HTML tags) | `false` | | `collapseBooleanAttributes` | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier#collapse_boolean_attributes) | `false` | +| `customFragmentQuantifierLimit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `1000` | | `collapseInlineTagWhitespace` | Don’t leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` | | `collapseWhitespace` | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` | | `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. | `ignoreCustomFragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g. ``, `{{ ... }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` | | `includeAutoGeneratedTags` | Insert tags generated by HTML parser | `true` | | `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` | +| `maxInputLength` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` | | `maxLineLength` | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points | | `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)`) | | `minifyJS` | Minify JavaScript in script elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) | @@ -120,6 +122,20 @@ Most of the options are disabled by default. Minifier options like `sortAttributes` and `sortClassName` won’t impact the plain-text size of the output. However, they form long repetitive chains of characters that should improve compression ratio of gzip used in HTTP compression. +## Security + +### ReDoS Protection + +This minifier includes protection against Regular Expression Denial of Service (ReDoS) attacks: + +* **Custom Fragment Quantifier Limits**: The `customFragmentQuantifierLimit` option (default: 1000) prevents exponential backtracking by using bounded quantifiers instead of unlimited ones (`*` or `+`) in regular expressions. + +* **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. + +* **Custom Fragment Warnings**: The minifier will warn you if your custom fragments contain unlimited quantifiers that could be vulnerable to ReDoS attacks. + +**Important:** When using custom `ignoreCustomFragments`, ensure your regular expressions don’t contain unlimited quantifiers (`*`, `+`) without bounds, as these can lead to ReDoS vulnerabilities. + ## Special cases ### Ignoring chunks of markup From b5aa1da0c0bc5678c09848383e4009d223029887 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 14 Jun 2025 21:03:38 +0200 Subject: [PATCH 11/13] fix: fix typo Signed-off-by: Jens Oliver Meiert --- tests/minifier.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/minifier.spec.js b/tests/minifier.spec.js index 9e389360..f5170bc1 100644 --- a/tests/minifier.spec.js +++ b/tests/minifier.spec.js @@ -2658,7 +2658,7 @@ test('conservative collapse', async () => { })).toBe(output); }); -test('collapse preseving a line break', async () => { +test('collapse preserving a line break', async () => { let input, output; input = '\n\n\n \n\n' + @@ -3644,4 +3644,4 @@ test('ReDoS prevention in custom fragments processing', async () => { expect(endTime3 - startTime3).toBeLessThan(2000); expect(result3).toContain(''); expect(result3).toContain(''); -}); +}); \ No newline at end of file From bd446c24293783fb3ecbe5e5178615fe70b3aa81 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 21 Jun 2025 13:12:33 +0200 Subject: [PATCH 12/13] docs: edit README headings and restructure security section Updated the headings to follow a consistent format and restructured the security section for clarity and readability. Minor adjustments were made to improve information accessibility and invite contributions for further improvements. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- README.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b9062dd8..9ed930df 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Pe | [W3C](https://www.w3.org/) | 51 | **36** | 42 | n/a | | [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) | 114 | **100** | 107 | n/a | -## Options Quick Reference +## Options quick reference Most of the options are disabled by default. @@ -122,20 +122,6 @@ Most of the options are disabled by default. Minifier options like `sortAttributes` and `sortClassName` won’t impact the plain-text size of the output. However, they form long repetitive chains of characters that should improve compression ratio of gzip used in HTTP compression. -## Security - -### ReDoS Protection - -This minifier includes protection against Regular Expression Denial of Service (ReDoS) attacks: - -* **Custom Fragment Quantifier Limits**: The `customFragmentQuantifierLimit` option (default: 1000) prevents exponential backtracking by using bounded quantifiers instead of unlimited ones (`*` or `+`) in regular expressions. - -* **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. - -* **Custom Fragment Warnings**: The minifier will warn you if your custom fragments contain unlimited quantifiers that could be vulnerable to ReDoS attacks. - -**Important:** When using custom `ignoreCustomFragments`, ensure your regular expressions don’t contain unlimited quantifiers (`*`, `+`) without bounds, as these can lead to ReDoS vulnerabilities. - ## Special cases ### Ignoring chunks of markup @@ -170,6 +156,22 @@ Output of resulting markup (e.g. `

foo

`) 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. +## Security + +### ReDoS protection + +This minifier includes protection against Regular Expression Denial of Service (ReDoS) attacks: + +* Custom fragment quantifier limits: The `customFragmentQuantifierLimit` option (default: `1000`) prevents exponential backtracking by using bounded quantifiers instead of unlimited ones (`*` or `+`) in regular expressions. + +* 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. + +* Custom fragment warnings: The minifier will warn you if your custom fragments contain unlimited quantifiers that could be vulnerable to ReDoS attacks. + +**Important:** When using custom `ignoreCustomFragments`, ensure your regular expressions don’t contain unlimited quantifiers (`*`, `+`) without bounds, as these can lead to ReDoS vulnerabilities. + +(Further improvements are needed. Contributions welcome.) + ## Running benchmarks Benchmarks for minified HTML: From 53086896fd4ccbafea2daaf3fc2f33a34b737310 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Sat, 21 Jun 2025 13:37:43 +0200 Subject: [PATCH 13/13] feat: reduce default quantifier limit to enhance ReDoS protection Lowered the `customFragmentQuantifierLimit` default from 1000 to 200 for improved protection against regular expression denial of service (ReDoS) attacks. Updated relevant documentation and CLI descriptions to reflect this change. Added detailed examples and enhanced guidance for defining safe custom fragment patterns. (This commit message was AI-generated.) Signed-off-by: Jens Oliver Meiert --- README.md | 51 +++++++++++++++++++++++++++++++++++++----- cli.js | 2 +- src/htmlminifier.js | 2 +- tests/minifier.spec.js | 2 +- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9ed930df..6e67e723 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HTML Minifier Next (HTMLMinifier) -[![NPM version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next) +[![npm version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next) (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.) @@ -78,7 +78,7 @@ Most of the options are disabled by default. | --- | --- | --- | | `caseSensitive` | Treat attributes in case sensitive manner (useful for custom HTML tags) | `false` | | `collapseBooleanAttributes` | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier#collapse_boolean_attributes) | `false` | -| `customFragmentQuantifierLimit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `1000` | +| `customFragmentQuantifierLimit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` | | `collapseInlineTagWhitespace` | Don’t leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` | | `collapseWhitespace` | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier#collapse_whitespace) | `false` | | `conservativeCollapse` | Always collapse to 1 space (never remove it entirely). Must be used in conjunction with `collapseWhitespace=true` | `false` | @@ -160,18 +160,59 @@ HTMLMinifier can’t know that original markup was only half of the tree; it doe ### ReDoS protection -This minifier includes protection against Regular Expression Denial of Service (ReDoS) attacks: +This minifier includes protection against regular expression denial of service (ReDoS) attacks: -* Custom fragment quantifier limits: The `customFragmentQuantifierLimit` option (default: `1000`) prevents exponential backtracking by using bounded quantifiers instead of unlimited ones (`*` or `+`) in regular expressions. +* Custom fragment quantifier limits: The `customFragmentQuantifierLimit` option (default: 200) prevents exponential backtracking by replacing unlimited quantifiers (`*`, `+`) with bounded ones in regular expressions. * 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. -* Custom fragment warnings: The minifier will warn you if your custom fragments contain unlimited quantifiers that could be vulnerable to ReDoS attacks. +* Enhanced pattern detection: The minifier detects and warns about various ReDoS-prone patterns including nested quantifiers, alternation with quantifiers, and multiple unlimited quantifiers. **Important:** When using custom `ignoreCustomFragments`, ensure your regular expressions don’t contain unlimited quantifiers (`*`, `+`) without bounds, as these can lead to ReDoS vulnerabilities. (Further improvements are needed. Contributions welcome.) +#### Custom fragment examples + +**Safe patterns** (recommended): + +```javascript +ignoreCustomFragments: [ + /<%[\s\S]{0,1000}?%>/, // JSP/ASP with explicit bounds + /<\?php[\s\S]{0,5000}?\?>/, // PHP with bounds + /\{\{[^}]{0,500}\}\}/ // Handlebars without nested braces +] +``` + +**Potentially unsafe patterns** (will trigger warnings): + +```javascript +ignoreCustomFragments: [ + /<%[\s\S]*?%>/, // Unlimited quantifiers + //, // Could cause issues with very long comments + /\{\{.*?\}\}/, // Nested unlimited quantifiers + /(script|style)[\s\S]*?/ // Multiple unlimited quantifiers +] +``` + +**Template engine configurations:** + +```javascript +// Handlebars/Mustache +ignoreCustomFragments: [/\{\{[\s\S]{0,1000}?\}\}/] + +// Liquid (Jekyll) +ignoreCustomFragments: [/\{%[\s\S]{0,500}?%\}/, /\{\{[\s\S]{0,500}?\}\}/] + +// Angular +ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/] + +// Vue.js +ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/] +``` + +**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. + ## Running benchmarks Benchmarks for minified HTML: diff --git a/cli.js b/cli.js index 71c7d3a4..3ccfcc22 100755 --- a/cli.js +++ b/cli.js @@ -101,7 +101,7 @@ function parseString(value) { const mainOptions = { caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)', collapseBooleanAttributes: 'Omit attribute values from boolean attributes', - customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks', parseInt], + customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks (default: 200)', parseInt], collapseInlineTagWhitespace: 'Collapse white space around inline tag', collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.', conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)', diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 97b4683b..6482a7b6 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -902,7 +902,7 @@ async function minifyHTML(value, options, partialMarkup) { } // Safe approach: Use bounded quantifiers instead of unlimited ones to prevent ReDoS - const maxQuantifier = options.customFragmentQuantifierLimit || 1000; + const maxQuantifier = options.customFragmentQuantifierLimit || 200; const whitespacePattern = `\\s{0,${maxQuantifier}}`; // Use bounded quantifiers to prevent ReDoS - this approach prevents exponential backtracking diff --git a/tests/minifier.spec.js b/tests/minifier.spec.js index f5170bc1..ec3f9c93 100644 --- a/tests/minifier.spec.js +++ b/tests/minifier.spec.js @@ -3644,4 +3644,4 @@ test('ReDoS prevention in custom fragments processing', async () => { expect(endTime3 - startTime3).toBeLessThan(2000); expect(result3).toContain(''); expect(result3).toContain(''); -}); \ No newline at end of file +});