From 149550935c63a98c11f27f694a7c4a9479e53794 Mon Sep 17 00:00:00 2001 From: Jamie Davis Date: Mon, 19 Mar 2018 17:07:17 -0400 Subject: [PATCH] security: Fix REDOS vulnerability Problem: As disclosed by email, the regex in this module was vulnerable to catastrophic backtracking. This could be used as a REDOS vector for Node.js servers that use this module. Solution: Use a two-step validation process. Split the vulnerable regex into three safe ones. --- index.js | 26 +++++++++++++++++++++++--- test/index.js | 11 +++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 1ef5187..1d3e349 100644 --- a/index.js +++ b/index.js @@ -6,10 +6,15 @@ module.exports = isUrl; /** - * Matcher. + * RegExps. + * A URL must match #1 and then at least one of #2/#3. + * Use two levels of REs to avoid REDOS. */ -var matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/; +var protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/; + +var localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/ +var nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/; /** * Loosely validate a URL `string`. @@ -19,5 +24,20 @@ var matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/; */ function isUrl(string){ - return matcher.test(string); + var match = string.match(protocolAndDomainRE); + if (!match) { + return false; + } + + var everythingAfterProtocol = match[1]; + if (!everythingAfterProtocol) { + return false; + } + + if (localhostDomainRE.test(everythingAfterProtocol) || + nonLocalhostDomainRE.test(everythingAfterProtocol)) { + return true; + } + + return false; } diff --git a/test/index.js b/test/index.js index 5f7aebc..52cfacb 100644 --- a/test/index.js +++ b/test/index.js @@ -119,4 +119,15 @@ describe('is-url', function () { assert(!url('google.com')); }); }); + + describe('redos', function () { + it('redos exploit', function () { + // Invalid. This should be discovered in under 1 second. + var attackString = 'a://localhost' + '9'.repeat(100000) + '\t'; + var before = process.hrtime(); + assert(!url(attackString), 'attackString was valid'); + var elapsed = process.hrtime(before); + assert(elapsed[0] < 1, 'attackString took ' + elapsed[0] + ' > 1 seconds'); + }); + }); });