Skip to content
Permalink
Browse files Browse the repository at this point in the history
Updated to include encrypting cookies, better protection client side
  • Loading branch information
valexandersaulys committed Oct 6, 2022
1 parent ae134ea commit 8eead6d
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 83 deletions.
33 changes: 27 additions & 6 deletions README.md
Expand Up @@ -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
Expand All @@ -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
)
Expand All @@ -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
Expand Down Expand Up @@ -68,3 +84,8 @@ app.post("/", (req, res) => {
return res.status(200).send("Got it!");
});
```


## License

[MIT](#)
57 changes: 57 additions & 0 deletions encryption.js
@@ -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
};
45 changes: 23 additions & 22 deletions index.js
@@ -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
Expand All @@ -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;
2 changes: 1 addition & 1 deletion package.json
@@ -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": {
Expand Down

0 comments on commit 8eead6d

Please sign in to comment.