CVE-2022-39287 (published 2022-10-07T20:15Z)

tiny-csrf is a Node.js cross site request forgery (CSRF) protection middleware. In versions prior to 1.1.0 cookies were not encrypted and thus CSRF tokens were transmitted in the clear. This issue has been addressed in commit `8eead6d` and the patch with be included in version 1.1.0. Users are advised to upgrade. There are no known workarounds for this issue.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-319
cwe_name: Cleartext Transmission of Sensitive Information
description: The product transmits sensitive or security-critical data in cleartext in a communication channel that can be sniffed by unauthorized actors.
extended_description: 
	 Many communication channels can be "sniffed" (monitored) by adversaries during data transmission. For example, in networking, packets can traverse many intermediary nodes from the source to the destination, whether across the internet, an internal network, the cloud, etc. Some actors might have privileged access to a network interface or any link along the channel, such as a router, but they might not be authorized to collect the underlying data. As a result, network traffic could be sniffed by adversaries, spilling security-critical data.
	 Applicable communication channels are not limited to software products. Applicable channels include hardware-specific technologies such as internal hardware networks and external debug channels, supporting remote JTAG debugging. When mitigations are not applied to combat adversaries within the product's threat model, this weakness significantly lowers the difficulty of exploitation by such adversaries.
	 When full communications are recorded or logged, such as with a packet dump, an adversary could attempt to obtain the dump long after the transmission has occurred and try to "sniff" the cleartext from the recorded communications in the dump itself. Even if the information is encoded in a way that is not human-readable, certain techniques could determine which encoding is being used, then decode the information.

         
         

Fix information:
repo_url: https://github.com/valexandersaulys/tiny-csrf
commit_details: repo_url: https://github.com/valexandersaulys/tiny-csrf
author: Vincent Alexander Saulys
author_date: 2022-10-06 12:10:13-04:00
msg: Updated to include encrypting cookies, better protection client side
repository: repo_url: https://github.com/valexandersaulys/tiny-csrf
repo_name: valexandersaulys/tiny-csrf
description: Tiny CSRF for express js applications
repo_language: JavaScript
owner: valexandersaulys
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,14 @@ This is a tiny csrf library meant to replace what `csurf` used to do
 [before it was deleted](https://github.com/expressjs/csurf). It is
 _almost_ a drop-in replacement.  
 
+Note that if you require very specific security needs you may want to
+look elsewhere. This library supports encrypting cookies on the client
+side to prevent malicious attackers from looking in. It does not
+protect against things such as [double submit
+cookies](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie). Those
+setups require greater setup and more know-how. This library aims to
+be simple to setup. 
+
 
 
 ## Installation
@@ -17,11 +25,16 @@ To Use in your app:
 ```javascript
 const csurf = require("tiny-csrf");
 const express = require("express");
+const session = require("express-session");
 
 let app = express();
 
+app.use(
+  session({})
+);
 app.use(
   csurf(
+    "123456789iamasecret987654321look",  // secret -- must be 32 bits or chars in length
     ["POST"],    // the request methods we want CSRF protection for
     ["/detail", /\/detail\.*/i]  // any URLs we want to exclude, either as strings or regexp
   )
@@ -30,13 +43,16 @@ app.use(
 // declare all your other routes and middleware
 ```
 
-Defaults to only requiring CSRF protection on `POST` requests and
-excludes no URLs. 
+The secret must be 32 bits (e.g. characters) in length and uses 
+[the built-in `crypto.createCipheriv` library built into Node
+](https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options). The
+secret length is enforced by the
+[`AES-256-CBC`](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)
+algorithm. 
 
-This uses the built-in [`crypto`
-library](https://nodejs.org/api/crypto.html#cryptorandomuuidoptions)
-for generating CSRF tokens. This may or may not be sufficient for your
-needs. 
+Defaults to only requiring CSRF protection on `POST`, `PUT`, and `PATCH` requests and
+excludes no URLs. The csrf will be checked for in the body of a
+request via `_csrf`. 
 
 
 ## Examples
@@ -68,3 +84,8 @@ app.post("/", (req, res) => {
   return res.status(200).send("Got it!");
 });
 ```
+
+
+## License
+
+[MIT](#)

programming_language: Markdown
filename: encryption.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,57 @@
+const { randomBytes, createCipheriv, createDecipheriv } = require("crypto");
+
+const ALGORITHM = "aes-256-cbc";
+
+const encryptCookie = (cookie, _secret) => {
+  /**
+   * Encrypt a cookie using AES 256 bits
+   * @param {cookie} string the cookie we want to encrypt. Will be visible as plain string to client.
+   * @param {_secret} string the secret that will be stored server-side. Client will never see this.
+   */
+  const iv = randomBytes(16);
+  const _cipher = createCipheriv(ALGORITHM, Buffer.from(_secret), iv);
+  const encrypted = [
+    iv.toString("hex"),
+    ":",
+    _cipher.update(cookie, "utf8", "hex"),
+    _cipher.final("hex")
+  ];
+  return encrypted.join("");
+};
+
+const decryptCookie = (cookie, _secret) => {
+  /**
+   * Decrypt a cookie using AES 256 bits
+   * @param {cookie} string the cookie we want to encrypt. Will be visible as plain string to client.
+   * @param {_secret} string the secret that will be stored server-side. Client will never see this.
+   */
+  const _encryptedArray = cookie.split(":");
+  if (_encryptedArray.length != 2) throw new Error("bad decrypt");
+  const iv = new Buffer(_encryptedArray[0], "hex");
+  const encrypted = new Buffer(_encryptedArray[1], "hex");
+  const decipher = createDecipheriv(ALGORITHM, _secret, iv);
+  const decrypted =
+    decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
+  return decrypted;
+};
+
+const verifyCsrf = (requestCsrf, cookieCsrf, _secret) => {
+  /**
+   * Verify a CSRF token
+   * @param {requestCsrf} string the CSRF coming from client side
+   * @param {cookieCsrf} string the CSRF as stored in the user's cookies
+   * @param {_secret} string the string used to encrypt the CSRF in the first place.
+   */
+  try {
+    const decryptedCookie = decryptCookie(cookieCsrf, _secret);
+    return decryptedCookie === requestCsrf;
+  } catch (err) {
+    return false;
+  }
+};
+
+module.exports = {
+  encryptCookie,
+  decryptCookie,
+  verifyCsrf
+};

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,19 @@
 const { randomUUID } = require("crypto");
+const { encryptCookie, verifyCsrf } = require("./encryption");
 
-const csurf = (forbiddenMethods, excludedUrls) => {
-  if (!forbiddenMethods) forbiddenMethods = ["POST"];
+const cookieParams = {
+  httpOnly: true,
+  sameSite: "strict",
+  signed: true,
+  maxAge: 300000
+};
+
+const csurf = (secret, forbiddenMethods, excludedUrls) => {
+  if (!forbiddenMethods) forbiddenMethods = ["POST", "PUT", "PATCH"];
+  if (secret.length != 32)
+    throw new Error("Your secret is not the required 32 characters long");
   return (req, res, next) => {
-    if (!req.cookies || !res.cookie)
+    if (!req.cookies || !res.cookie || !req.signedCookies)
       throw new Error("No Cookie middleware is installed");
     if (
       // if any excludedUrl matches as either string or regexp
@@ -12,42 +22,33 @@ const csurf = (forbiddenMethods, excludedUrls) => {
       ).length > 0
     ) {
       req.csrfToken = () => {
-        if (!req.cookies.csrfToken) {
-          const csrfToken = randomUUID();
-          res.cookie("csrfToken", csrfToken);
-          return csrfToken;
-        }
-        return req.cookies.csrfToken;
+        const csrfToken = randomUUID();
+        res.cookie("csrfToken", encryptCookie(csrfToken, secret), cookieParams);
+        return csrfToken;
       };
       return next();
     } else if (forbiddenMethods.includes(req.method)) {
-      const { csrfToken } = req.cookies;
+      const { csrfToken } = req.signedCookies;
       if (
         csrfToken != undefined &&
-        (req.query._csrf === csrfToken ||
-          req.params._csrf === csrfToken ||
-          req.body._csrf === csrfToken)
+        verifyCsrf(req.body?._csrf, csrfToken, secret)
       ) {
-        res.cookie("csrfToken", "");
+        res.cookie("csrfToken", null, cookieParams);
         return next();
       } else {
         throw new Error(
-          `Did not get a CSRF token for ${req.method} ${req.originalUrl}: ${req.body._csrf} v. ${csrfToken}`
+          `Did not get a valid CSRF token for '${req.method} ${req.originalUrl}': ${req.body?._csrf} v. ${csrfToken}`
         );
       }
     } else {
       req.csrfToken = () => {
-        if (!req.cookies.csrfToken) {
-          const csrfToken = randomUUID();
-          res.cookie("csrfToken", csrfToken);
-          return csrfToken;
-        }
-        return req.cookies.csrfToken;
+        const csrfToken = randomUUID();
+        res.cookie("csrfToken", encryptCookie(csrfToken, secret), cookieParams);
+        return csrfToken;
       };
       return next();
     }
   };
 };
 
-// module.exports = csurf(forbiddenMethods, excludedUrls);
 module.exports = csurf;

programming_language: TypeScript
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 {
   "name": "tiny-csrf",
-  "version": "1.0.3",
+  "version": "1.1.0",
   "description": "Tiny CSRF library for use with ExpressJS",
   "main": "index.js",
   "scripts": {

programming_language: JavaScript
filename: test.js
change_type: ModificationType.MODIFY
diff: @@ -3,13 +3,57 @@ const { describe, before, beforeEach, after, afterEach, it } = require("mocha");
 const sinon = require("sinon");
 const { mockRequest, mockResponse } = require("mock-req-res");
 
+const { randomUUID, randomBytes } = require("crypto");
 const csurf = require("./index");
+const { encryptCookie, decryptCookie, verifyCsrf } = require("./encryption");
+
+describe("Cookie Encryption Tests", () => {
+  before(() => {});
+  after(() => {});
+
+  it("will encrypt and decrypt a cookie", () => {
+    // req.cookies.csrfToken =>  will be scrambled
+    const secret = "123456789iamasecret987654321look";
+    const csrfToken = randomUUID();
+    const encryptedCsrfToken = encryptCookie(csrfToken, secret);
+    assert.notEqual(
+      csrfToken,
+      encryptedCsrfToken,
+      "Both are equal as plain string and shouldn't be"
+    );
+    assert.isTrue(verifyCsrf(csrfToken, encryptedCsrfToken, secret));
+    assert.isFalse(
+      verifyCsrf(
+        csrfToken,
+        encryptedCsrfToken,
+        randomBytes(16).toString("hex")
+      ),
+      "Should not be able to verify without the secret"
+    );
+    assert.isFalse(
+      verifyCsrf(randomUUID(), encryptedCsrfToken, secret),
+      "Should not verify random UUID tokens"
+    );
+    assert.isFalse(
+      verifyCsrf("", encryptedCsrfToken, secret),
+      "Should not verify random blank tokens"
+    );
+    assert.isFalse(
+      verifyCsrf(null, encryptedCsrfToken, secret),
+      "Should not verify random null tokens"
+    );
+  });
+});
 
 describe("Default Options Tests", () => {
   before(() => {
-    this.csrf = csurf(null, []);
+    this.secret = "123456789iamasecret987654321look";
+    this.csrf = csurf(this.secret);
   });
 
+  it("throw internal error if our secret is not long enough", () => {
+    assert.throws(() => csurf("imnotlongenough"));
+  });
   it("throws internal error if we have no cookies", () => {
     const req = mockRequest({
       cookies: null
@@ -28,7 +72,7 @@ describe("Default Options Tests", () => {
   it("generates token for non-POST request", () => {
     const req = mockRequest({
       method: "GET",
-      cookies: {},
+      signedCookies: {},
       body: {}
     });
     const res = mockResponse({
@@ -46,8 +90,8 @@ describe("Default Options Tests", () => {
   it("allows if the CSRF token is correct", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaa", this.secret)
       },
       body: {
         _csrf: "aaaa"
@@ -64,8 +108,8 @@ describe("Default Options Tests", () => {
   it("does not allow if the CSRF token is incorrect", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaabbb"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaabbb", this.secret)
       },
       body: {
         _csrf: "aaaa"
@@ -80,15 +124,15 @@ describe("Default Options Tests", () => {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
   it("does not allow if the CSRF token is missing in body", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaabbb"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaabbb", this.secret)
       },
       body: {}
     });
@@ -101,14 +145,14 @@ describe("Default Options Tests", () => {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
   it("does not allow if the CSRF token was never generated", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {},
+      signedCookies: {},
       body: {}
     });
     const res = mockResponse({
@@ -120,44 +164,23 @@ describe("Default Options Tests", () => {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
-  it("will reuse if the CSRF token already exists in a non-POST", () => {
-    const req = mockRequest({
-      method: "GET",
-      cookies: {
-        csrfToken: "aaaabc"
-      },
-      body: {}
-    });
-    const res = mockResponse({
-      cookie: sinon.stub()
-    });
-    const next = sinon.stub();
-    assert.isNotFunction(req.csrfToken);
-    this.csrf(req, res, next);
-    assert.isTrue(next.calledOnce);
-    assert.isFunction(
-      req.csrfToken,
-      "Did not attach a csrfToken function to req"
-    );
-    const sampleToken = req.csrfToken();
-    assert.equal(sampleToken, "aaaabc");
-  });
 });
 
 describe("Tests w/Specified Included Request Methods", () => {
   before(() => {
-    this.csrf = csurf(["POST", "PUT"], []);
+    this.secret = "123456789iamasecret987654321look";
+    this.csrf = csurf(this.secret, ["POST", "PUT"], []);
   });
 
   it("allows if the CSRF token is correct", () => {
     const req = mockRequest({
       method: "PUT",
-      cookies: {
-        csrfToken: "aaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaa", this.secret)
       },
       body: {
         _csrf: "aaaa"
@@ -174,8 +197,8 @@ describe("Tests w/Specified Included Request Methods", () => {
   it("does not allow if the CSRF token is incorrect", () => {
     const req = mockRequest({
       method: "PUT",
-      cookies: {
-        csrfToken: "aaaabbb"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaabbb", this.secret)
       },
       body: {
         _csrf: "aaaa"
@@ -190,15 +213,15 @@ describe("Tests w/Specified Included Request Methods", () => {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
   it("allows if the method is specified as not included", () => {
     const req = mockRequest({
       method: "GET",
-      cookies: {
-        csrfToken: "aaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaa", this.secret)
       },
       body: {
         _csrf: ""
@@ -216,14 +239,15 @@ describe("Tests w/Specified Included Request Methods", () => {
 
 describe("Tests w/Specified Excluded URLs", () => {
   before(() => {
-    this.csrf = csurf(null, ["/detail", /\/detail\.*/i]);
+    this.secret = "123456789iamasecret987654321look";
+    this.csrf = csurf(this.secret, null, ["/detail", /\/detail\.*/i]);
   });
 
   it("allows if the URL is marked as excluded", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaaa", this.secret)
       },
       body: {
         _csrf: ""
@@ -241,14 +265,12 @@ describe("Tests w/Specified Excluded URLs", () => {
       req.csrfToken,
       "Did not add the csrfToken to this request"
     );
-    const sampleToken = req.csrfToken();
-    assert.equal(sampleToken, "aaaaa", "Not reusing csrf tokens");
   });
   it("allows if the URL is marked as excluded as a regexp", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaaa", this.secret)
       },
       body: {
         _csrf: ""
@@ -266,13 +288,11 @@ describe("Tests w/Specified Excluded URLs", () => {
       req.csrfToken,
       "Did not add the csrfToken to this request"
     );
-    const sampleToken = req.csrfToken();
-    assert.equal(sampleToken, "aaaaa", "Not reusing csrf tokens");
   });
   it("generates a new token if no token is supplied", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {},
+      signedCookies: {},
       body: {
         _csrf: ""
       },
@@ -295,7 +315,7 @@ describe("Tests w/Specified Excluded URLs", () => {
   it("does not allow if the CSRF token is incorrect and the URL is not marked as excluded", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
+      signedCookies: {
         csrfToken: "aaaabbb"
       },
       body: {
@@ -312,7 +332,7 @@ describe("Tests w/Specified Excluded URLs", () => {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });

programming_language: TypeScript