diff --git a/src/utils/validator.ts b/src/utils/validator.ts index fb738904e1..15b2f53e68 100644 --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -15,8 +15,6 @@ * limitations under the License. */ -import url = require('url'); - /** * Validates that a value is a byte buffer. * @@ -234,33 +232,38 @@ export function isURL(urlStr: any): boolean { return false; } try { - const uri = url.parse(urlStr); + const uri = new URL(urlStr); const scheme = uri.protocol; - const slashes = uri.slashes; - const hostname = uri.hostname; - const pathname = uri.pathname; - if ((scheme !== 'http:' && scheme !== 'https:') || !slashes) { + if (scheme !== 'http:' && scheme !== 'https:') { return false; } - // Validate hostname: Can contain letters, numbers, underscore and dashes separated by a dot. - // Each zone must not start with a hyphen or underscore. - if (!hostname || !/^[a-zA-Z0-9]+[\w-]*([.]?[a-zA-Z0-9]+[\w-]*)*$/.test(hostname)) { - return false; + const hostname = uri.hostname; + // Validate hostname strictly to match previous behavior and prevent weak/invalid domains. + // Must be alphanumeric with optional dashes/underscores, separated by dots. + // Cannot start/end with dot or dash (mostly). + // This regex is safe (no nested quantifiers with overlap). + if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/.test(hostname)) { + // Check for IPv6 literals which are valid but behave differently. + // Node 'new URL' keeps brackets for IPv6: [::1] -> [::1] + // Check for IPv6 address (simple check for brackets) + if (!/^\[[a-fA-F0-9:.]+\]$/.test(hostname)) { + return false; + } } - // Allow for pathnames: (/chars+)*/? + // Restore strict pathname validation: (/chars+)*/? // Where chars can be a combination of: a-z A-Z 0-9 - _ . ~ ! $ & ' ( ) * + , ; = : @ % const pathnameRe = /^(\/[\w\-.~!$'()*+,;=:@%]+)*\/?$/; // Validate pathname. + const pathname = uri.pathname; if (pathname && - pathname !== '/' && - !pathnameRe.test(pathname)) { + pathname !== '/' && + !pathnameRe.test(pathname)) { return false; } - // Allow any query string and hash as long as no invalid character is used. + return true; } catch (e) { return false; } - return true; } diff --git a/test/unit/utils/validator.spec.ts b/test/unit/utils/validator.spec.ts index f059839ca9..9c07648087 100644 --- a/test/unit/utils/validator.spec.ts +++ b/test/unit/utils/validator.spec.ts @@ -530,3 +530,28 @@ describe('isISODateString()', () => { expect(isISODateString(validISODateString)).to.be.true; }); }); + +describe('isURL() ReDoS and Long Inputs', () => { + it('should handle long valid URLs quickly', function () { + this.timeout(1000); + const longUrl = 'https://' + Array(50).fill('a').join('.') + '.com'; + expect(isURL(longUrl)).to.be.true; + }); + + it('should handle long invalid URLs quickly (ReDoS check)', function () { + this.timeout(1000); + const longInvalid = 'https://' + 'a'.repeat(22) + '!'; + expect(isURL(longInvalid)).to.be.false; + }); + + it('should handle very long domain with many segments', function () { + this.timeout(1000); + const manySegments = 'https://' + Array(100).fill('a').join('.') + '.com'; + expect(isURL(manySegments)).to.be.true; + }); + + it('should reject invalid dot usage caught by strict regex', function () { + expect(isURL('https://a.b')).to.be.true; + expect(isURL('https://a..b')).to.be.false; + }); +});