:lock: fix: CVE-2023-26115 by aashutoshrathi · Pull Request #33 · jonschlinkert/word-wrap (original) (raw)

Merged

doowb

merged 2 commits into

Jul 18, 2023

Conversation

@aashutoshrathi

Fixes #32

Issue with existing approach

CVE-2023-26115
TLDR; ReDoS, the issue is due to Regex matching algo.

image

Approach used here

Removed the use of String.prototype.replace(Regex), instead wrote function to trim tabs and space from the end of line.

Benchmarks

Code snippet used

const wrap = require("word-wrap");

const trimTabAndSpaces = (str) => { const lines = str.split('\n'); const trimmedLines = lines.map(line => { let i = line.length - 1; while (i >= 0 && (line[i] === ' ' || line[i] === '\t')) { i--; } return line.substring(0, i + 1); }); return trimmedLines.join('\n'); }

for (let i = 0; i <= 10; i++) { const attack = "a" + "t".repeat(i * 10_00000); const start = performance.now(); attack.replace(/[ \t]*$/gm, ''); // -> since this is happening inside code. // wrap(attack, { trim: true }); console.log(wrap.trim: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>a</mi><mi>t</mi><mi>t</mi><mi>a</mi><mi>c</mi><mi>k</mi><mi mathvariant="normal">.</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>g</mi><mi>t</mi><mi>h</mi></mrow><mi>c</mi><mi>h</mi><mi>a</mi><mi>r</mi><mi>a</mi><mi>c</mi><mi>t</mi><mi>e</mi><mi>r</mi><mi>s</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">{attack.length} characters: </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">a</span><span class="mord mathnormal">tt</span><span class="mord mathnormal">a</span><span class="mord mathnormal">c</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span></span><span class="mord mathnormal">c</span><span class="mord mathnormal">ha</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">a</span><span class="mord mathnormal">c</span><span class="mord mathnormal">t</span><span class="mord mathnormal">ers</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{performance.now() - start}ms); }

for (i = 0; i <= 10; i++) { const attack = "a" + "t".repeat(i * 10_00000); const start2 = performance.now(); trimTabAndSpaces(attack); console.log(trimTabAndSpaces: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>a</mi><mi>t</mi><mi>t</mi><mi>a</mi><mi>c</mi><mi>k</mi><mi mathvariant="normal">.</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>g</mi><mi>t</mi><mi>h</mi></mrow><mi>c</mi><mi>h</mi><mi>a</mi><mi>r</mi><mi>a</mi><mi>c</mi><mi>t</mi><mi>e</mi><mi>r</mi><mi>s</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">{attack.length} characters: </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">a</span><span class="mord mathnormal">tt</span><span class="mord mathnormal">a</span><span class="mord mathnormal">c</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span></span><span class="mord mathnormal">c</span><span class="mord mathnormal">ha</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">a</span><span class="mord mathnormal">c</span><span class="mord mathnormal">t</span><span class="mord mathnormal">ers</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{performance.now() - start2}ms); }

Performance Benchmarks

node test.js wrap.trim: 1 characters: 0.015165984630584717ms wrap.trim: 1000001 characters: 6.47087499499321ms wrap.trim: 2000001 characters: 13.000333994626999ms wrap.trim: 3000001 characters: 19.574499994516373ms wrap.trim: 4000001 characters: 26.035459011793137ms wrap.trim: 5000001 characters: 31.902458995580673ms wrap.trim: 6000001 characters: 38.429375022649765ms wrap.trim: 7000001 characters: 44.44049999117851ms wrap.trim: 8000001 characters: 51.241291999816895ms wrap.trim: 9000001 characters: 57.14437499642372ms wrap.trim: 10000001 characters: 63.937458992004395ms

trimTabAndSpaces: 1 characters: 0.3221670091152191ms trimTabAndSpaces: 1000001 characters: 0.2049579918384552ms trimTabAndSpaces: 2000001 characters: 0.3135409951210022ms trimTabAndSpaces: 3000001 characters: 0.3718339800834656ms trimTabAndSpaces: 4000001 characters: 0.7508749961853027ms trimTabAndSpaces: 5000001 characters: 0.6123340129852295ms trimTabAndSpaces: 6000001 characters: 0.7714170217514038ms trimTabAndSpaces: 7000001 characters: 1.168584018945694ms trimTabAndSpaces: 8000001 characters: 0.9755829870700836ms trimTabAndSpaces: 9000001 characters: 1.406792014837265ms trimTabAndSpaces: 10000001 characters: 1.5417499840259552ms

htbkoo, OGC-Zang, TooTallNate, LeoniePhiline, bennycode, alfaproject, bryan-hoang, pimterry, davidcalhoun, SchroederSteffen, and 9 more reacted with thumbs up emoji Altair200333, sfc-gh-dszmolka, jackkoppa, am-maneaters, nergal, MateuszKikmunter, Greg-Smulko, swellander, Luen, wellwelwel, and 13 more reacted with heart emoji

@aashutoshrathi

sergeyt

}).join(newline);
if (options.trim === true) {
result = result.replace(/[ \t]*$/gm, '');

Choose a reason for hiding this comment

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

I guess /\s+$/gm works faster

Choose a reason for hiding this comment

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

Thanks, let me check on that.
But ideally, I would like to not use regex for the operation that can be easily handled by native trimEnd, right?

Choose a reason for hiding this comment

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

maybe or native better regex can be faster. need to bench to check that

Choose a reason for hiding this comment

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

Hi @aashutoshrathi, sorry for the long delay, and thanks for the PR. I'll try to get this merged in ASAP.

I guess /\s+$/gm works faster

Yes, let's use this pattern without the m flag. Using + will also be a bit safer since it won't allow that pattern to match an empty string.

Choose a reason for hiding this comment

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

/[ \t]+$/g is slightly better than /\s+$/g it would seem. Unfortunately both regexes are suspectable to redos, as opposed to trimEnd();

$ time node -e 'payloadstr=""; for ( i=0; i < 100000; i++ ) payloadstr+="\t"; payloadstr+="a"; payloadstr.replace(/[ \t]+$/gm,"");'
real 0m21.084s
$ time node -e 'payloadstr=""; for ( i=0; i < 100000; i++ ) payloadstr+="\t"; payloadstr+="a"; payloadstr.replace(/\s+$/g,"");'
real 0m8.587s
$ time node -e 'payloadstr=""; for ( i=0; i < 100000; i++ ) payloadstr+="\t"; payloadstr+="a"; payloadstr.replace(/[ \t]+$/g,"");'
real 0m6.169s
$ time node -e 'payloadstr=""; for ( i=0; i < 100000; i++ ) payloadstr+="\t"; payloadstr+="a"; payloadstr.trimEnd();'
real 0m0.082s

Choose a reason for hiding this comment

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

gurungkiran

*/
function trimTabAndSpaces(str) {
const lines = str.split('\n');

Choose a reason for hiding this comment

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

may be using method chaining eliminates the need to create an intermediate variable ??
return str.split('\n').map(line => line.trimEnd()).join('\n');

Choose a reason for hiding this comment

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

yup! can be done. I once just need verification the approach or whether it is important or not

Choose a reason for hiding this comment

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

utilizing native String.prototype.trimEnd() would require the node-engine to be at least on version 10.0.0.

Ref: String.prototype.trimEnd() - JavaScript | MDN

This should probably considered a breaking-change which requires new major version release.
Hence I would recommend to utilize custom implementation for this patch.

Choose a reason for hiding this comment

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

oh! I can write trimEnd myself if that's the case

Choose a reason for hiding this comment

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

For the reason mentioned by @mscheid-sf, for now let's merge in the regex I suggested in my other comment. After that I'd be happy to take another PR that replaces the regex with .trimEnd() (Using .trimEnd would indeed qualify as a breaking change according to semver, and also it's generally a good practice to minimize potential for regressions when releasing patches).

@lepetitpatron

Do you have an update when this PR will be merged? Thanks!

AndyLinNZ, emilyuhde, Davilink, bgswilde, am-maneaters, mscheid-sf, jjoshm, othomann, sfc-gh-dszmolka, unamaria, and 19 more reacted with thumbs up emoji qrabigail, velsonjr, lukaselmer, Oceanestars, and aaleksandrov reacted with eyes emoji

@jainanuj0812

@sergeyt

I suggest going with /\s+$/gm faster regexp first. Previous one is slower since it uses * quantifier which includes empty replacements as well which is redundant ops

@aashutoshrathi

@MateuszKikmunter

Any update on this @jonschlinkert ? We're blocked by this issue not being fixed.

@xavisegura

@sergeyt

Any update on this @jonschlinkert ? Is this repo active?

in meantime someone brave enough can publish a version of the package that can be a replacement in package.json. for example, like we can do for class-validator:

"class-transformer": "npm:@nestjs/class-transformer@0.4.0",
"class-validator": "npm:@nestjs/class-validator@0.13.4",

@aashutoshrathi

Any update on this @jonschlinkert ? Is this repo active?

in meantime someone brave enough can publish a version of the package that can be a replacement in package.json. for example, like we can do for class-validator:

"class-transformer": "npm:@nestjs/class-transformer@0.4.0",
"class-validator": "npm:@nestjs/class-validator@0.13.4",

I think we can try, what do you think @sergeyt ?

@sergeyt

Any update on this @jonschlinkert ? Is this repo active?

in meantime someone brave enough can publish a version of the package that can be a replacement in package.json. for example, like we can do for class-validator:

"class-transformer": "npm:@nestjs/class-transformer@0.4.0",
"class-validator": "npm:@nestjs/class-validator@0.13.4",

I think we can try, what do you think @sergeyt ?

Sure, why not

@aashutoshrathi

@jainanuj0812

@icbat icbat mentioned this pull request

Apr 17, 2023

@aashutoshrathi

Okay here's a temporary package: https://www.npmjs.com/package/@aashutoshrathi/word-wrap In package.json, the below should do it.

"resolutions": { "word-wrap": "npm:@aashutoshrathi/word-wrap@1.2.4" },

Please use the newer version the older one causes some issues

"resolutions": { "word-wrap": "npm:@aashutoshrathi/word-wrap@1.2.5" },

@andreidiaconescu

@sfc-gh-dszmolka

If we use npm not yarn, how would we use overrides to change a package with another ? i do not find it clear from the official docs: https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides Thank you.

for example, using something like this in your package.json:

"overrides": { "word-wrap" : "npm:@aashutoshrathi/word-wrap@1.2.5" },

@DiegoTavelli

Okay here's a temporary package: https://www.npmjs.com/package/@aashutoshrathi/word-wrap In package.json, the below should do it.

"resolutions": { "word-wrap": "npm:@aashutoshrathi/word-wrap@1.2.4" },

Please use the newer version the older one causes some issues

"resolutions": { "word-wrap": "npm:@aashutoshrathi/word-wrap@1.2.5" },

I will try with this one, I have a question. Becouse that dependency is involved into eslint and jest dependecies, I have to specify from the example parent dependency. 'jest/jest-config.../word-wrap' or just specify as you say ?

@aashutoshrathi

You can use the same, without specifying the path.

@velsonjr

Please have this merged to the main branch

@jainanuj0812

@velsonjr

@aashutoshrathi , The same is still reappearing for the following path, react-scripts@5.0.1 › jest@27.5.1 › @jest/core@27.5.1 › jest-config@27.5.1 › jest-environment-jsdom@27.5.1 › jsdom@16.7.0 › escodegen@2.0.0 › optionator@0.8.3 › word-wrap@1.2.5

@aashutoshrathi

@aashutoshrathi , The same is still reappearing for the following path, react-scripts@5.0.1 › jest@27.5.1 › @jest/core@27.5.1 › jest-config@27.5.1 › jest-environment-jsdom@27.5.1 › jsdom@16.7.0 › escodegen@2.0.0 › optionator@0.8.3 › word-wrap@1.2.5

Hey! if you use resolution override in the main package.json, it shouldn't happen

@velsonjr

Hey @aashutoshrathi, In case do another npm i, it throws me this npm ERR! Invalid Version: npm:@aashutoshrathi/word-wrap@1.2.5 . I have the added the same inside resolutions. Also when I upgrade the react, node and npm versions, the scan still shows the vulnerability ( In my case its react 18.2, node 16.20 and npm 8.19)

@xx745

Hi all, why is this fix not merged yet?

@bgswilde

@xx745, I don't think @jonschlinkert has any desire to continue managing this repo. I've reached out to him several times on twitter, where he's active, but no response. I would love to be wrong though! I would be great for him to come in and merge this or for there to be a way for somebody else to be an admin here, then he doesn't have to keep up with it.

This was referenced

Jul 19, 2023

@wellwelwel

@aashutoshrathi, since this PR has been merged and this Issue was resolved with PR #41, I'm backing to the original project now.

Thank you for bringing a solution until this problem was actually solved here, I believe your fork has helped many people and keep helping (the numbers themselves prove this) 🚀

@aashutoshrathi

@aashutoshrathi, since this PR has been merged and this Issue was resolved with PR #41, I'm backing to the original project now.

Thank you for bringing a solution until this problem was actually solved here, I believe your fork has helped many people and keep helping (the numbers themselves prove this) 🚀

No problemo anytime for OSS folks! 🖖🏻

@lentz lentz mentioned this pull request

Jul 19, 2023

@bgswilde

Thanks @nicholas-quirk-mass-gov , I was just coming here to comment.

We choose to go with #41 since it's using the inline trimEnd implementation for backwards compatibility. That PR was branched off of this PR, so GitHub automatically merged in the changes here when I merged in the other PR.

I'm publishing word-wrap@1.2.4 in a few minutes with the fix from #41.

@doowb and @nicholas-quirk-mass-gov, just asking for clarity. @nicholas-quirk-mass-gov, are you saying that PR #41 (1.2.4) didn't fix the vulnerability?

@aashutoshrathi

Thanks @nicholas-quirk-mass-gov , I was just coming here to comment.
We choose to go with #41 since it's using the inline trimEnd implementation for backwards compatibility. That PR was branched off of this PR, so GitHub automatically merged in the changes here when I merged in the other PR.
I'm publishing word-wrap@1.2.4 in a few minutes with the fix from #41.

@doowb and @nicholas-quirk-mass-gov, just asking for clarity. @nicholas-quirk-mass-gov, are you saying that PR #41 (1.2.4) didn't fix the vulnerability?

Yes! This PR won't fix it. until we leave Regex completely.
My first commit was better than the second after the requested changes, IMO won't fix it.

@wellwelwel

@bgswilde, understanding the storyline 🧙🏻

This PR changes only the regex rule:

The @aashutoshrathi/word-wrap fork uses the native trimEnd.

The PR #41 recreates the trimTabAndSpaces from @aashutoshrathi/word-wrap fork removing the regex rules and adding a custom trimEnd looking for backwards compatibility.

Both @aashutoshrathi/word-wrap fork and PR #41 fix that vulnerability, this PR (#33) don't.

For a better understand, please see the #41 changes.

This was referenced

Jul 20, 2023

This was referenced

Jul 31, 2023

github-actions Bot pushed a commit to brafdlog/caspion that referenced this pull request

Dec 4, 2023

@semantic-release-bot

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters

[ Show hidden characters]({{ revealButtonHref }})