CVE-2016-2515: Resource Management Errors (CWE-399)
value: Hawk before 3.1.3 and 4.x before 4.1.1 allow remote attackers to cause a denial of service (CPU consumption or partial outage) via a long (1) header or (2) URI that is matched against an improper regular expression.
CWE description: Weaknesses in this category are related to improper management of system resources.
CVSS v3 base score: 7.5
Published: 2016-04-13T16:59Z
Repository: https://github.com/hueniverse/hawk
Language: JavaScript
Files changed: lib/server.js, lib/utils.js, package.json, test/server.js, test/utils.js
Commit date: 2016-01-19 11:53:57 -0800
Commit message: Fix minor DoS attack on long headers or uris. Closes #168

Diff: diff --git a/lib/server.js b/lib/server.js
index e42c884..0486805 100755
--- a/lib/server.js
+++ b/lib/server.js
@@ -308,10 +308,15 @@ exports.header = function (credentials, artifacts, options) {
 /*
  * Arguments and options are the same as authenticate() with the exception that the only supported options are:
  * 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port'
  */
 
+
+//                       1     2             3           4
+internals.bewitRegex = /^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/;
+
+
 exports.authenticateBewit = function (req, credentialsFunc, options, callback) {
 
     callback = Hoek.nextTick(callback);
 
     // Application time
@@ -325,12 +330,15 @@ exports.authenticateBewit = function (req, credentialsFunc, options, callback) {
         return callback(Boom.badRequest(request.message));
     }
 
     // Extract bewit
 
-    //                                 1     2             3           4
-    const resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/);
+    if (request.url.length > Utils.limits.maxMatchLength) {
+        return callback(Boom.badRequest('Resource path exceeds max length'));
+    }
+
+    const resource = request.url.match(internals.bewitRegex);
     if (!resource) {
         return callback(Utils.unauthorized());
     }
 
     // Bewit not empty
diff --git a/lib/utils.js b/lib/utils.js
index 29c8a22..ecb64d3 100755
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -15,10 +15,15 @@ exports.version = function () {
 
     return require('../package.json').version;
 };
 
 
+exports.limits = {
+    maxMatchLength: 4096            // Limit the length of uris and headers to avoid a DoS attack on string matching
+};
+
+
 // Extract host and port from request
 
 //                                            $1                            $2
 internals.hostHeaderRegex = /^(?:(?:\r\n)?\s)*((?:[^:]+)|(?:\[[^\]]+\]))(?::(\d+))?(?:(?:\r\n)?\s)*$/;              // (IPv4, hostname)|(IPv6)
 
@@ -29,10 +34,14 @@ exports.parseHost = function (req, hostHeaderName) {
     const hostHeader = req.headers[hostHeaderName];
     if (!hostHeader) {
         return null;
     }
 
+    if (hostHeader.length > exports.limits.maxMatchLength) {
+        return null;
+    }
+
     const hostParts = hostHeader.match(internals.hostHeaderRegex);
     if (!hostParts) {
         return null;
     }
 
@@ -98,21 +107,29 @@ exports.nowSecs = function (localtimeOffsetMsec) {
 
     return Math.floor(exports.now(localtimeOffsetMsec) / 1000);
 };
 
 
+internals.authHeaderRegex = /^(\w+)(?:\s+(.*))?$/;                                      // Header: scheme[ something]
+internals.attributeRegex = /^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/;   // !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
+
+
 // Parse Hawk HTTP Authorization header
 
 exports.parseAuthorizationHeader = function (header, keys) {
 
     keys = keys || ['id', 'ts', 'nonce', 'hash', 'ext', 'mac', 'app', 'dlg'];
 
     if (!header) {
         return Boom.unauthorized(null, 'Hawk');
     }
 
-    const headerParts = header.match(/^(\w+)(?:\s+(.*))?$/);       // Header: scheme[ something]
+    if (header.length > exports.limits.maxMatchLength) {
+        return Boom.badRequest('Header length too long');
+    }
+
+    const headerParts = header.match(internals.authHeaderRegex);
     if (!headerParts) {
         return Boom.badRequest('Invalid header syntax');
     }
 
     const scheme = headerParts[1];
@@ -134,13 +151,13 @@ exports.parseAuthorizationHeader = function (header, keys) {
         if (keys.indexOf($1) === -1) {
             errorMessage = 'Unknown attribute: ' + $1;
             return;
         }
 
-        // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
+        // Allowed attribute value characters
 
-        if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
+        if ($2.match(internals.attributeRegex) === null) {
             errorMessage = 'Bad attribute value: ' + $1;
             return;
         }
 
         // Check for duplicates
diff --git a/package.json b/package.json
index 58d5832..1538f7e 100755
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
 {
   "name": "hawk",
   "description": "HTTP Hawk Authentication Scheme",
-  "version": "4.1.0",
+  "version": "4.1.1",
   "author": "Eran Hammer <eran@hammer.io> (http://hueniverse.com)",
   "repository": "git://github.com/hueniverse/hawk",
   "main": "lib/index.js",
   "browser": "dist/browser.js",
   "keywords": [
diff --git a/test/server.js b/test/server.js
index f8f06d0..c88d9b5 100755
--- a/test/server.js
+++ b/test/server.js
@@ -969,10 +969,37 @@ describe('Server', () => {
             expect(header).to.equal('');
             done();
         });
     });
 
+    describe('authenticateBewit()', () => {
+
+        it('errors on uri too long', (done) => {
+
+            let long = '/';
+            for (let i = 0; i < 5000; ++i) {
+                long += 'x';
+            }
+
+            const req = {
+                method: 'GET',
+                url: long,
+                host: 'example.com',
+                port: 8080,
+                authorization: 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"'
+            };
+
+            Hawk.server.authenticateBewit(req, credentialsFunc, {}, (err, credentials, bewit) => {
+
+                expect(err).to.exist();
+                expect(err.output.statusCode).to.equal(400);
+                expect(err.message).to.equal('Resource path exceeds max length');
+                done();
+            });
+        });
+    });
+
     describe('authenticateMessage()', () => {
 
         it('errors on invalid authorization (ts)', (done) => {
 
             credentialsFunc('123456', (err, credentials1) => {
diff --git a/test/utils.js b/test/utils.js
index b7d517c..d7a265c 100755
--- a/test/utils.js
+++ b/test/utils.js
@@ -93,10 +93,38 @@ describe('Utils', () => {
             const host = Hawk.utils.parseHost(req, 'Host');
             expect(host.port).to.equal('8000');
             expect(host.name).to.equal('[123:123:123]');
             done();
         });
+
+        it('errors on header too long', (done) => {
+
+            let long = '';
+            for (let i = 0; i < 5000; ++i) {
+                long += 'x';
+            }
+
+            expect(Hawk.utils.parseHost({ headers: { host: long } })).to.be.null();
+            done();
+        });
+    });
+
+    describe('parseAuthorizationHeader()', () => {
+
+        it('errors on header too long', (done) => {
+
+            let long = 'Scheme a="';
+            for (let i = 0; i < 5000; ++i) {
+                long += 'x';
+            }
+            long += '"';
+
+            const err = Hawk.utils.parseAuthorizationHeader(long, ['a']);
+            expect(err).to.be.instanceof(Error);
+            expect(err.message).to.equal('Header length too long');
+            done();
+        });
     });
 
     describe('version()', () => {
 
         it('returns the correct package version number', (done) => {

Vulnerable code:     //                                 1     2             3           4
    const resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/);
    const headerParts = header.match(/^(\w+)(?:\s+(.*))?$/);       // Header: scheme[ something]
        // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
        if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
  "version": "4.1.0",
Fixed code: 
//                       1     2             3           4
internals.bewitRegex = /^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/;


    if (request.url.length > Utils.limits.maxMatchLength) {
        return callback(Boom.badRequest('Resource path exceeds max length'));
    }

    const resource = request.url.match(internals.bewitRegex);
exports.limits = {
    maxMatchLength: 4096            // Limit the length of uris and headers to avoid a DoS attack on string matching
};


    if (hostHeader.length > exports.limits.maxMatchLength) {
        return null;
    }

internals.authHeaderRegex = /^(\w+)(?:\s+(.*))?$/;                                      // Header: scheme[ something]
internals.attributeRegex = /^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/;   // !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9


    if (header.length > exports.limits.maxMatchLength) {
        return Boom.badRequest('Header length too long');
    }

    const headerParts = header.match(internals.authHeaderRegex);
        // Allowed attribute value characters
        if ($2.match(internals.attributeRegex) === null) {
  "version": "4.1.1",
    describe('authenticateBewit()', () => {

        it('errors on uri too long', (done) => {

            let long = '/';
            for (let i = 0; i < 5000; ++i) {
                long += 'x';
            }

            const req = {
                method: 'GET',
                url: long,
                host: 'example.com',
                port: 8080,
                authorization: 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"'
            };

            Hawk.server.authenticateBewit(req, credentialsFunc, {}, (err, credentials, bewit) => {

                expect(err).to.exist();
                expect(err.output.statusCode).to.equal(400);
                expect(err.message).to.equal('Resource path exceeds max length');
                done();
            });
        });
    });


        it('errors on header too long', (done) => {

            let long = '';
            for (let i = 0; i < 5000; ++i) {
                long += 'x';
            }

            expect(Hawk.utils.parseHost({ headers: { host: long } })).to.be.null();
            done();
        });
    });

    describe('parseAuthorizationHeader()', () => {

        it('errors on header too long', (done) => {

            let long = 'Scheme a="';
            for (let i = 0; i < 5000; ++i) {
                long += 'x';
            }
            long += '"';

            const err = Hawk.utils.parseAuthorizationHeader(long, ['a']);
            expect(err).to.be.instanceof(Error);
            expect(err.message).to.equal('Header length too long');
            done();
        });