add more validation to addCookie parameters v2.1-dev
authorJustin Wind <justin.wind+git@gmail.com>
Fri, 24 May 2024 23:10:53 +0000 (16:10 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Fri, 24 May 2024 23:10:53 +0000 (16:10 -0700)
lib/common.js
test/lib/common.js

index c6c21efc705cf7581dfd351ffe8ba3a8275cbdb1..2f0aeb835694b5eb0f5e6130b8974ff645c585b6 100644 (file)
@@ -205,6 +205,12 @@ const unfoldHeaderLines = (lines) => {
   return lines;
 };
 
   return lines;
 };
 
+const validTokenRE = /^[!#$%&'*+-.0-9A-Z^_`a-z~]+$/;
+const validValueRE = /^[!#$%&'()*+-./0-9:<=>?@A-Z[\]^_`a-z{|}~]*$/;
+const validPathRE = /^[ !"#$%&'()*+,-./0-9:<=>?@A-Z[\\\]^_`a-z{|}~]*$/;
+const validLabelRE = /^[a-zA-Z0-9-]+$/;
+const invalidLabelRE = /--|^-|-$/;
+
 /**
  * Adds a new set-cookie header value to response, with supplied data.
  * @param {http.ServerResponse} res response
 /**
  * Adds a new set-cookie header value to response, with supplied data.
  * @param {http.ServerResponse} res response
@@ -219,6 +225,7 @@ const unfoldHeaderLines = (lines) => {
  * @param {string=} opt.path cookie path
  * @param {string=} opt.sameSite cookie sharing
  * @param {boolean=} opt.secure cookie security
  * @param {string=} opt.path cookie path
  * @param {string=} opt.sameSite cookie sharing
  * @param {boolean=} opt.secure cookie security
+ * @param {string[]=} opt.extension cookie extension attribute values
  */
 function addCookie(res, name, value, opt = {}) {
   const options = {
  */
 function addCookie(res, name, value, opt = {}) {
   const options = {
@@ -229,39 +236,83 @@ function addCookie(res, name, value, opt = {}) {
     path: undefined,
     sameSite: undefined,
     secure: false,
     path: undefined,
     sameSite: undefined,
     secure: false,
+    extension: [],
     ...opt,
   };
     ...opt,
   };
-  // TODO: validate name, value
+
+  if (!validTokenRE.test(name)) {
+    throw new RangeError('invalid cookie name');
+  }
+
+  if (value.startsWith('"') && value.endsWith('"')) {
+    if (!validValueRE.test(value.slice(1, value.length - 1))) {
+      throw new RangeError('invalid cookie value');
+    };
+  } else if (!validValueRE.test(value)) {
+    throw new RangeError('invalid cookie value');
+  } 
+
   const cookieParts = [
     `${name}=${value}`,
   ];
   const cookieParts = [
     `${name}=${value}`,
   ];
+
   if (options.domain) {
   if (options.domain) {
+    for (const label of options.domain.split('.')) {
+      if (!validLabelRE.test(label) || invalidLabelRE.test(label)) {
+        throw new RangeError('invalid cookie domain');
+      }
+
+    }
     cookieParts.push(`Domain=${options.domain}`);
   }
     cookieParts.push(`Domain=${options.domain}`);
   }
+
   if (options.expires) {
     if (!(options.expires instanceof Date)) {
       throw new TypeError('cookie expires must be Date');
     }
     cookieParts.push(`Expires=${options.expires.toUTCString()}`);
   }
   if (options.expires) {
     if (!(options.expires instanceof Date)) {
       throw new TypeError('cookie expires must be Date');
     }
     cookieParts.push(`Expires=${options.expires.toUTCString()}`);
   }
+
   if (options.httpOnly) {
     cookieParts.push('HttpOnly');
   }
   if (options.httpOnly) {
     cookieParts.push('HttpOnly');
   }
+
   if (options.maxAge) {
     cookieParts.push(`Max-Age=${options.maxAge}`);
   }
   if (options.maxAge) {
     cookieParts.push(`Max-Age=${options.maxAge}`);
   }
+
   if (options.path) {
   if (options.path) {
+    if (!validPathRE.test(options.path)) {
+      throw new RangeError('cookie path value not valid');
+    }
     cookieParts.push(`Path=${options.path}`);
   }
     cookieParts.push(`Path=${options.path}`);
   }
+
   if (options.sameSite) {
     if (!(['Strict', 'Lax', 'None'].includes(options.sameSite))) {
       throw new RangeError('cookie sameSite value not valid');
     }
   if (options.sameSite) {
     if (!(['Strict', 'Lax', 'None'].includes(options.sameSite))) {
       throw new RangeError('cookie sameSite value not valid');
     }
+    if (options.sameSite === 'None'
+    &&  !options.secure) {
+      throw new RangeError('cookie with sameSite None must also be secure');
+    }
     cookieParts.push(`SameSite=${options.sameSite}`);
   }
     cookieParts.push(`SameSite=${options.sameSite}`);
   }
+
   if (options.secure) {
     cookieParts.push('Secure');
   }
   if (options.secure) {
     cookieParts.push('Secure');
   }
+
+  if (!Array.isArray(options.extension)) {
+    throw new TypeError('cookie extension must be Array');
+  }
+  for (const extension of options.extension) {
+    if (!validPathRE.test(extension)) {
+      throw new RangeError('cookie extension value not valid');
+    }
+    cookieParts.push(extension);
+  }
+
   res.appendHeader(Enum.Header.SetCookie, cookieParts.join('; '));
 }
 
   res.appendHeader(Enum.Header.SetCookie, cookieParts.join('; '));
 }
 
index 68f2599791c12faf5d106656200fd23cb7c55947..06d711d186fdc07469e89311a0ca2207d769540c 100644 (file)
@@ -371,6 +371,7 @@ describe('common', function () {
         path: '/foo',
         sameSite: 'Lax',
         secure: true,
         path: '/foo',
         sameSite: 'Lax',
         secure: true,
+        extension: ['Mischief'],
       });
       assert(res.appendHeader.called);
     });
       });
       assert(res.appendHeader.called);
     });
@@ -378,7 +379,39 @@ describe('common', function () {
       assert.throws(() => common.addCookie(res, name, value, { expires: 'never' }), TypeError);
     });
     it('covers invalid sameSite', function () {
       assert.throws(() => common.addCookie(res, name, value, { expires: 'never' }), TypeError);
     });
     it('covers invalid sameSite', function () {
-      assert.throws(() => common.addCookie(res, name, value, { sameSite: 'Whatever' }));
+      assert.throws(() => common.addCookie(res, name, value, { sameSite: 'Whatever' }), RangeError);
+    });
+    it('covers invalid sameSite/secure setting', function () {
+      assert.throws(() => common.addCookie(res, name, value, { sameSite: 'None', secure: false }), RangeError);
+    });
+    it('covers invalid path', function () {
+      assert.throws(() => common.addCookie(res, name, value, { path: '/bad;path' }), RangeError);
+    });
+    it('covers invalid domain', function () {
+      assert.throws(() => common.addCookie(res, name, value, { domain: 'a-.com' }), RangeError);
+    });
+    it('covers invalid extension type', function () {
+      assert.throws(() => common.addCookie(res, name, value, { extension: 'extension' }), TypeError);
+    });
+    it('covers invalid extension', function () {
+      assert.throws(() => common.addCookie(res, name, value, { extension: ['bad;extension'] }), RangeError);
+    });
+    it('covers invalid name', function () {
+      name = 'bad:name';
+      assert.throws(() => common.addCookie(res, name, value), RangeError);
+    });
+    it('covers invalid value', function () {
+      value = 'bad;value';
+      assert.throws(() => common.addCookie(res, name, value), RangeError);
+    });
+    it('covers quoted value', function () {
+      value = '"value"';
+      common.addCookie(res, name, value);
+      assert(res.appendHeader.called);
+    });
+    it('covers invalid quoted value', function () {
+      value = '"bad;value"';
+      assert.throws(() => common.addCookie(res, name, value), RangeError);
     });
     it('covers no options', function () {
       common.addCookie(res, name, value);
     });
     it('covers no options', function () {
       common.addCookie(res, name, value);