From 871fe1193c46bd4ce50d5a80427e2920dabdd3f7 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 7 Nov 2024 16:24:27 -0600 Subject: [PATCH 1/3] Revert "Archive code" This reverts commit 1cee470c2781727a5cf25a24c4f0fd3207a3ff2b. --- .github/workflows/ci.yml | 165 +++++++++++++ README.md | 321 ++++++++++++++++++++++++ index.js | 297 +++++++++++++++++++++++ package.json | 38 ++- test/.eslintrc.yml | 2 + test/test.js | 511 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 1333 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 index.js create mode 100644 test/.eslintrc.yml create mode 100644 test/test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c6261c2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,165 @@ +name: ci + +on: +- pull_request +- push + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + name: + - Node.js 0.8 + - Node.js 0.10 + - Node.js 0.12 + - io.js 1.x + - io.js 2.x + - io.js 3.x + - Node.js 4.x + - Node.js 5.x + - Node.js 6.x + - Node.js 7.x + - Node.js 8.x + - Node.js 9.x + - Node.js 10.x + - Node.js 11.x + - Node.js 12.x + - Node.js 13.x + - Node.js 14.x + - Node.js 15.x + - Node.js 16.x + + include: + - name: Node.js 0.8 + node-version: "0.8" + npm-i: mocha@2.5.3 supertest@1.1.0 + npm-rm: nyc + + - name: Node.js 0.10 + node-version: "0.10" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 0.12 + node-version: "0.12" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 1.x + node-version: "1.8" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 2.x + node-version: "2.5" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 3.x + node-version: "3.3" + npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 4.x + node-version: "4.9" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 5.x + node-version: "5.12" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 6.x + node-version: "6.17" + npm-i: mocha@6.2.2 nyc@14.1.1 + + - name: Node.js 7.x + node-version: "7.10" + npm-i: mocha@6.2.2 nyc@14.1.1 + + - name: Node.js 8.x + node-version: "8.17" + npm-i: mocha@7.2.0 + + - name: Node.js 9.x + node-version: "9.11" + npm-i: mocha@7.2.0 + + - name: Node.js 10.x + node-version: "10.18" + + - name: Node.js 11.x + node-version: "11.15" + + - name: Node.js 12.x + node-version: "12.22" + + - name: Node.js 13.x + node-version: "13.14" + + - name: Node.js 14.x + node-version: "14.18" + + - name: Node.js 15.x + node-version: "15.14" + + - name: Node.js 16.x + node-version: "16.10" + + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js ${{ matrix.node-version }} + shell: bash -eo pipefail -l {0} + run: | + nvm install --default ${{ matrix.node-version }} + if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + nvm install --alias=npm 0.10 + nvm use ${{ matrix.node-version }} + sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" + npm config set strict-ssl false + fi + dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + + - name: Configure npm + run: npm config set shrinkwrap false + + - name: Remove npm module(s) ${{ matrix.npm-rm }} + run: npm rm --silent --save-dev ${{ matrix.npm-rm }} + if: matrix.npm-rm != '' + + - name: Install npm module(s) ${{ matrix.npm-i }} + run: npm install --save-dev ${{ matrix.npm-i }} + if: matrix.npm-i != '' + + - name: Setup Node.js version-specific dependencies + shell: bash + run: | + # eslint for linting + # - remove on Node.js < 10 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ + grep -E '^eslint(-|$)' | \ + sort -r | \ + xargs -n1 npm rm --silent --save-dev + fi + + - name: Install Node.js dependencies + run: npm install + + - name: List environment + id: list_env + shell: bash + run: | + echo "node@$(node -v)" + echo "npm@$(npm -v)" + npm -s ls ||: + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' + + - name: Run tests + shell: bash + run: | + if npm -ps ls nyc | grep -q nyc; then + npm run test-ci + else + npm test + fi + + - name: Lint code + if: steps.list_env.outputs.eslint != '' + run: npm run lint \ No newline at end of file diff --git a/README.md b/README.md index 8b57079..12a38d1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,24 @@ # csurf +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][node-url] +[![Build status][github-actions-ci-image]][github-actions-ci-url] +[![Test coverage][coveralls-image]][coveralls-url] + +Node.js [CSRF][wikipedia-csrf] protection middleware. + +Requires either a session middleware or [cookie-parser](https://www.npmjs.com/package/cookie-parser) to be initialized first. + + * If you are setting the ["cookie" option](#cookie) to a non-`false` value, + then you must use [cookie-parser](https://www.npmjs.com/package/cookie-parser) + before this module. + * Otherwise, you must use a session middleware before this module. For example: + - [express-session](https://www.npmjs.com/package/express-session) + - [cookie-session](https://www.npmjs.com/package/cookie-session) + +If you have questions on how this module is implemented, please read +[Understanding CSRF](https://github.com/pillarjs/understanding-csrf). + ## Deprecated This npm module is currently deprecated due to the large influx of security vulunerability reports @@ -8,3 +27,305 @@ project does not have the resources to put into this module, which is largely un SPA-based applications. Please instead use an alternative CSRF protection package if you do need one: https://www.npmjs.com/search?q=express%20csrf + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install csurf +``` + +## API + +```js +var csurf = require('csurf') +``` + +### csurf([options]) + +Create a middleware for CSRF token creation and validation. This middleware +adds a `req.csrfToken()` function to make a token which should be added to +requests which mutate state, within a hidden form field, query-string etc. +This token is validated against the visitor's session or csrf cookie. + +#### Options + +The `csurf` function takes an optional `options` object that may contain +any of the following keys: + +##### cookie + +Determines if the token secret for the user should be stored in a cookie +or in `req.session`. Storing the token secret in a cookie implements +the [double submit cookie pattern][owsap-csrf-double-submit]. +Defaults to `false`. + +When set to `true` (or an object of options for the cookie), then the module +changes behavior and no longer uses `req.session`. This means you _are no +longer required to use a session middleware_. Instead, you do need to use the +[cookie-parser](https://www.npmjs.com/package/cookie-parser) middleware in +your app before this middleware. + +When set to an object, cookie storage of the secret is enabled and the +object contains options for this functionality (when set to `true`, the +defaults for the options are used). The options may contain any of the +following keys: + + - `key` - the name of the cookie to use to store the token secret + (defaults to `'_csrf'`). + - `path` - the path of the cookie (defaults to `'/'`). + - `signed` - indicates if the cookie should be signed (defaults to `false`). + - `secure` - marks the cookie to be used with HTTPS only (defaults to + `false`). + - `maxAge` - the number of seconds after which the cookie will expire + (defaults to session length). + - `httpOnly` - flags the cookie to be accessible only by the web server + (defaults to `false`). + - `sameSite` - sets the same site policy for the cookie(defaults to + `false`). This can be set to `'strict'`, `'lax'`, `'none'`, or `true` + (which maps to `'strict'`). + - `domain` - sets the domain the cookie is valid on(defaults to current + domain). + +##### ignoreMethods + +An array of the methods for which CSRF token checking will disabled. +Defaults to `['GET', 'HEAD', 'OPTIONS']`. + +##### sessionKey + +Determines what property ("key") on `req` the session object is located. +Defaults to `'session'` (i.e. looks at `req.session`). The CSRF secret +from this library is stored and read as `req[sessionKey].csrfSecret`. + +If the ["cookie" option](#cookie) is not `false`, then this option does +nothing. + +##### value + +Provide a function that the middleware will invoke to read the token from +the request for validation. The function is called as `value(req)` and is +expected to return the token as a string. + +The default value is a function that reads the token from the following +locations, in order: + + - `req.body._csrf` - typically generated by the `body-parser` module. + - `req.query._csrf` - a built-in from Express.js to read from the URL + query string. + - `req.headers['csrf-token']` - the `CSRF-Token` HTTP request header. + - `req.headers['xsrf-token']` - the `XSRF-Token` HTTP request header. + - `req.headers['x-csrf-token']` - the `X-CSRF-Token` HTTP request header. + - `req.headers['x-xsrf-token']` - the `X-XSRF-Token` HTTP request header. + +## Example + +### Simple express example + +The following is an example of some server-side code that generates a form +that requires a CSRF token to post back. + +```js +var cookieParser = require('cookie-parser') +var csrf = require('csurf') +var bodyParser = require('body-parser') +var express = require('express') + +// setup route middlewares +var csrfProtection = csrf({ cookie: true }) +var parseForm = bodyParser.urlencoded({ extended: false }) + +// create express app +var app = express() + +// parse cookies +// we need this because "cookie" is true in csrfProtection +app.use(cookieParser()) + +app.get('/form', csrfProtection, function (req, res) { + // pass the csrfToken to the view + res.render('send', { csrfToken: req.csrfToken() }) +}) + +app.post('/process', parseForm, csrfProtection, function (req, res) { + res.send('data is being processed') +}) +``` + +Inside the view (depending on your template language; handlebars-style +is demonstrated here), set the `csrfToken` value as the value of a hidden +input field named `_csrf`: + +```html +
+ + + Favorite color: + +
+``` + +#### Using AJAX + +When accessing protected routes via ajax both the csrf token will need to be +passed in the request. Typically this is done using a request header, as adding +a request header can typically be done at a central location easily without +payload modification. + +The CSRF token is obtained from the `req.csrfToken()` call on the server-side. +This token needs to be exposed to the client-side, typically by including it in +the initial page content. One possibility is to store it in an HTML `` tag, +where value can then be retrieved at the time of the request by JavaScript. + +The following can be included in your view (handlebar example below), where the +`csrfToken` value came from `req.csrfToken()`: + +```html + +``` + +The following is an example of using the +[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to post +to the `/process` route with the CSRF token from the `` tag on the page: + + + +```js +// Read the CSRF token from the tag +var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content') + +// Make a request using the Fetch API +fetch('/process', { + credentials: 'same-origin', // <-- includes cookies in the request + headers: { + 'CSRF-Token': token // <-- is the csrf token as a header + }, + method: 'POST', + body: { + favoriteColor: 'blue' + } +}) +``` + +#### Single Page Application (SPA) + +Many SPA frameworks like Angular have CSRF support built in automatically. +Typically they will reflect the value from a specific cookie, like +`XSRF-TOKEN` (which is the case for Angular). + +To take advantage of this, set the value from `req.csrfToken()` in the cookie +used by the SPA framework. This is only necessary to do on the route that +renders the page (where `res.render` or `res.sendFile` is called in Express, +for example). + +The following is an example for Express of a typical SPA response: + +```js +app.all('*', function (req, res) { + res.cookie('XSRF-TOKEN', req.csrfToken()) + res.render('index') +}) +``` + +### Ignoring Routes + +**Note** CSRF checks should only be disabled for requests that you expect to +come from outside of your website. Do not disable CSRF checks for requests +that you expect to only come from your website. An existing session, even if +it belongs to an authenticated user, is not enough to protect against CSRF +attacks. + +The following is an example of how to order your routes so that certain endpoints +do not check for a valid CSRF token. + +```js +var cookieParser = require('cookie-parser') +var csrf = require('csurf') +var bodyParser = require('body-parser') +var express = require('express') + +// create express app +var app = express() + +// create api router +var api = createApiRouter() + +// mount api before csrf is appended to the app stack +app.use('/api', api) + +// now add csrf and other middlewares, after the "/api" was mounted +app.use(bodyParser.urlencoded({ extended: false })) +app.use(cookieParser()) +app.use(csrf({ cookie: true })) + +app.get('/form', function (req, res) { + // pass the csrfToken to the view + res.render('send', { csrfToken: req.csrfToken() }) +}) + +app.post('/process', function (req, res) { + res.send('csrf was required to get here') +}) + +function createApiRouter () { + var router = new express.Router() + + router.post('/getProfile', function (req, res) { + res.send('no csrf to get here') + }) + + return router +} +``` + +### Custom error handling + +When the CSRF token validation fails, an error is thrown that has +`err.code === 'EBADCSRFTOKEN'`. This can be used to display custom +error messages. + +```js +var bodyParser = require('body-parser') +var cookieParser = require('cookie-parser') +var csrf = require('csurf') +var express = require('express') + +var app = express() +app.use(bodyParser.urlencoded({ extended: false })) +app.use(cookieParser()) +app.use(csrf({ cookie: true })) + +// error handler +app.use(function (err, req, res, next) { + if (err.code !== 'EBADCSRFTOKEN') return next(err) + + // handle CSRF token errors here + res.status(403) + res.send('form tampered with') +}) +``` + +## References + +- [Cross-side request forgery on Wikipedia][wikipedia-csrf] +- [OWASP Cross-Site Request Forgery Prevention Cheat Sheet][owsap-csrf] + +[owsap-csrf]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html +[owsap-csrf-double-submit]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie +[wikipedia-csrf]: https://en.wikipedia.org/wiki/Cross-site_request_forgery + +## License + +[MIT](LICENSE) + +[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/csurf/master +[coveralls-url]: https://coveralls.io/r/expressjs/csurf?branch=master +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/csurf +[npm-url]: https://npmjs.org/package/csurf +[npm-version-image]: https://badgen.net/npm/v/csurf +[github-actions-ci-image]: https://badgen.net/github/checks/expressjs/csurf/master?label=ci +[github-actions-ci-url]: https://github.com/expressjs/csurf/actions/workflows/ci.yml diff --git a/index.js b/index.js new file mode 100644 index 0000000..367bdba --- /dev/null +++ b/index.js @@ -0,0 +1,297 @@ +/*! + * csurf + * Copyright(c) 2011 Sencha Inc. + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2014-2016 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var Cookie = require('cookie') +var createError = require('http-errors') +var sign = require('cookie-signature').sign +var Tokens = require('csrf') + +/** + * Module exports. + * @public + */ + +module.exports = csurf + +/** + * CSRF protection middleware. + * + * This middleware adds a `req.csrfToken()` function to make a token + * which should be added to requests which mutate + * state, within a hidden form field, query-string etc. This + * token is validated against the visitor's session. + * + * @param {Object} options + * @return {Function} middleware + * @public + */ + +function csurf (options) { + var opts = options || {} + + // get cookie options + var cookie = getCookieOptions(opts.cookie) + + // get session options + var sessionKey = opts.sessionKey || 'session' + + // get value getter + var value = opts.value || defaultValue + + // token repo + var tokens = new Tokens(opts) + + // ignored methods + var ignoreMethods = opts.ignoreMethods === undefined + ? ['GET', 'HEAD', 'OPTIONS'] + : opts.ignoreMethods + + if (!Array.isArray(ignoreMethods)) { + throw new TypeError('option ignoreMethods must be an array') + } + + // generate lookup + var ignoreMethod = getIgnoredMethods(ignoreMethods) + + return function csrf (req, res, next) { + // validate the configuration against request + if (!verifyConfiguration(req, sessionKey, cookie)) { + return next(new Error('misconfigured csrf')) + } + + // get the secret from the request + var secret = getSecret(req, sessionKey, cookie) + var token + + // lazy-load token getter + req.csrfToken = function csrfToken () { + var sec = !cookie + ? getSecret(req, sessionKey, cookie) + : secret + + // use cached token if secret has not changed + if (token && sec === secret) { + return token + } + + // generate & set new secret + if (sec === undefined) { + sec = tokens.secretSync() + setSecret(req, res, sessionKey, sec, cookie) + } + + // update changed secret + secret = sec + + // create new token + token = tokens.create(secret) + + return token + } + + // generate & set secret + if (!secret) { + secret = tokens.secretSync() + setSecret(req, res, sessionKey, secret, cookie) + } + + // verify the incoming token + if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) { + return next(createError(403, 'invalid csrf token', { + code: 'EBADCSRFTOKEN' + })) + } + + next() + } +} + +/** + * Default value function, checking the `req.body` + * and `req.query` for the CSRF token. + * + * @param {IncomingMessage} req + * @return {String} + * @api private + */ + +function defaultValue (req) { + return (req.body && req.body._csrf) || + (req.query && req.query._csrf) || + (req.headers['csrf-token']) || + (req.headers['xsrf-token']) || + (req.headers['x-csrf-token']) || + (req.headers['x-xsrf-token']) +} + +/** + * Get options for cookie. + * + * @param {boolean|object} [options] + * @returns {object} + * @api private + */ + +function getCookieOptions (options) { + if (options !== true && typeof options !== 'object') { + return undefined + } + + var opts = Object.create(null) + + // defaults + opts.key = '_csrf' + opts.path = '/' + + if (options && typeof options === 'object') { + for (var prop in options) { + var val = options[prop] + + if (val !== undefined) { + opts[prop] = val + } + } + } + + return opts +} + +/** + * Get a lookup of ignored methods. + * + * @param {array} methods + * @returns {object} + * @api private + */ + +function getIgnoredMethods (methods) { + var obj = Object.create(null) + + for (var i = 0; i < methods.length; i++) { + var method = methods[i].toUpperCase() + obj[method] = true + } + + return obj +} + +/** + * Get the token secret from the request. + * + * @param {IncomingMessage} req + * @param {String} sessionKey + * @param {Object} [cookie] + * @api private + */ + +function getSecret (req, sessionKey, cookie) { + // get the bag & key + var bag = getSecretBag(req, sessionKey, cookie) + var key = cookie ? cookie.key : 'csrfSecret' + + if (!bag) { + throw new Error('misconfigured csrf') + } + + // return secret from bag + return bag[key] +} + +/** + * Get the token secret bag from the request. + * + * @param {IncomingMessage} req + * @param {String} sessionKey + * @param {Object} [cookie] + * @api private + */ + +function getSecretBag (req, sessionKey, cookie) { + if (cookie) { + // get secret from cookie + var cookieKey = cookie.signed + ? 'signedCookies' + : 'cookies' + + return req[cookieKey] + } else { + // get secret from session + return req[sessionKey] + } +} + +/** + * Set a cookie on the HTTP response. + * + * @param {OutgoingMessage} res + * @param {string} name + * @param {string} val + * @param {Object} [options] + * @api private + */ + +function setCookie (res, name, val, options) { + var data = Cookie.serialize(name, val, options) + + var prev = res.getHeader('set-cookie') || [] + var header = Array.isArray(prev) ? prev.concat(data) + : [prev, data] + + res.setHeader('set-cookie', header) +} + +/** + * Set the token secret on the request. + * + * @param {IncomingMessage} req + * @param {OutgoingMessage} res + * @param {string} sessionKey + * @param {string} val + * @param {Object} [cookie] + * @api private + */ + +function setSecret (req, res, sessionKey, val, cookie) { + if (cookie) { + // set secret on cookie + var value = val + + if (cookie.signed) { + value = 's:' + sign(val, req.secret) + } + + setCookie(res, cookie.key, value, cookie) + } else { + // set secret on session + req[sessionKey].csrfSecret = val + } +} + +/** + * Verify the configuration against the request. + * @private + */ + +function verifyConfiguration (req, sessionKey, cookie) { + if (!getSecretBag(req, sessionKey, cookie)) { + return false + } + + if (cookie && cookie.signed && !req.secret) { + return false + } + + return true +} diff --git a/package.json b/package.json index 940242d..7eb22a4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,43 @@ "Douglas Christopher Wilson " ], "license": "MIT", - "repository": "expressjs/csurf", + "repository": "sailshq/csurf", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "1.8.0" + }, + "devDependencies": { + "body-parser": "1.19.0", + "connect": "3.7.0", + "cookie-parser": "1.4.6", + "cookie-session": "1.4.0", + "eslint": "7.32.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "4.3.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "8.2.1", + "nyc": "15.1.0", + "supertest": "4.0.2" + }, + "engines": { + "node": ">= 0.8.0" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --check-leaks --reporter spec --bail test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + }, + "files": [ + "HISTORY.md", + "LICENSE", + "index.js" + ], "keywords": [ "csrf", "tokens", diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml new file mode 100644 index 0000000..9808c3b --- /dev/null +++ b/test/.eslintrc.yml @@ -0,0 +1,2 @@ +env: + mocha: true diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..2e5d7b8 --- /dev/null +++ b/test/test.js @@ -0,0 +1,511 @@ + +process.env.NODE_ENV = 'test' + +var assert = require('assert') +var connect = require('connect') +var http = require('http') +var session = require('cookie-session') +var bodyParser = require('body-parser') +var cookieParser = require('cookie-parser') +var querystring = require('querystring') +var request = require('supertest') + +var csurf = require('..') + +describe('csurf', function () { + it('should work in req.body', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .send('_csrf=' + encodeURIComponent(token)) + .expect(200, done) + }) + }) + + it('should work in req.query', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + request(server) + .post('/?_csrf=' + encodeURIComponent(token)) + .set('Cookie', cookies(res)) + .expect(200, done) + }) + }) + + it('should work in csrf-token header', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('csrf-token', token) + .expect(200, done) + }) + }) + + it('should work in xsrf-token header', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('xsrf-token', token) + .expect(200, done) + }) + }) + + it('should work in x-csrf-token header', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('x-csrf-token', token) + .expect(200, done) + }) + }) + + it('should work in x-xsrf-token header', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('x-xsrf-token', token) + .expect(200, done) + }) + }) + + it('should fail with an invalid token', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', '42') + .expect(403, done) + }) + }) + + it('should fail with no token', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .post('/') + .set('Cookie', cookies(res)) + .expect(403, done) + }) + }) + + it('should provide error code on invalid token error', function (done) { + var app = connect() + app.use(session({ keys: ['a', 'b'] })) + app.use(csurf()) + + app.use(function (req, res) { + res.end(req.csrfToken() || 'none') + }) + + app.use(function (err, req, res, next) { + if (err.code !== 'EBADCSRFTOKEN') return next(err) + res.statusCode = 403 + res.end('session has expired or form tampered with') + }) + + request(app) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + request(app) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', String(res.text + 'p')) + .expect(403, 'session has expired or form tampered with', done) + }) + }) + + it('should error without session secret storage', function (done) { + var app = connect() + + app.use(csurf()) + + request(app) + .get('/') + .expect(500, /misconfigured csrf/, done) + }) + + describe('with "cookie" option', function () { + describe('when true', function () { + it('should store secret in "_csrf" cookie', function (done) { + var server = createServer({ cookie: true }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var data = cookie(res, '_csrf') + var token = res.text + + assert.ok(Boolean(data)) + assert.ok(/; *path=\/(?:;|$)/i.test(data)) + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', token) + .expect(200, done) + }) + }) + + it('should append cookie to existing Set-Cookie header', function (done) { + var app = connect() + + app.use(cookieParser('keyboard cat')) + app.use(function (req, res, next) { + res.setHeader('Set-Cookie', 'foo=bar') + next() + }) + app.use(csurf({ cookie: true })) + app.use(function (req, res) { + res.end(req.csrfToken() || 'none') + }) + + request(app) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + assert.ok(Boolean(cookie(res, '_csrf'))) + assert.ok(Boolean(cookie(res, 'foo'))) + + request(app) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', token) + .expect(200, done) + }) + }) + }) + + describe('when an object', function () { + it('should configure the cookie name with "key"', function (done) { + var server = createServer({ cookie: { key: '_customcsrf' } }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var data = cookie(res, '_customcsrf') + var token = res.text + + assert.ok(Boolean(data)) + assert.ok(/; *path=\/(?:;|$)/i.test(data)) + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', token) + .expect(200, done) + }) + }) + + it('should keep default cookie name when "key: undefined"', function (done) { + var server = createServer({ cookie: { key: undefined } }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var data = cookie(res, '_csrf') + var token = res.text + + assert.ok(Boolean(data)) + assert.ok(/; *path=\/(?:;|$)/i.test(data)) + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', token) + .expect(200, done) + }) + }) + + describe('when "signed": true', function () { + it('should enable signing', function (done) { + var server = createServer({ cookie: { signed: true } }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var data = cookie(res, '_csrf') + var token = res.text + + assert.ok(Boolean(data)) + assert.ok(/^_csrf=s%3A/i.test(data)) + + request(server) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', token) + .expect(200, done) + }) + }) + + it('should error without cookieParser', function (done) { + var app = connect() + + app.use(csurf({ cookie: { signed: true } })) + + request(app) + .get('/') + .expect(500, /misconfigured csrf/, done) + }) + + it('should error when cookieParser is missing secret', function (done) { + var app = connect() + + app.use(cookieParser()) + app.use(csurf({ cookie: { signed: true } })) + + request(app) + .get('/') + .expect(500, /misconfigured csrf/, done) + }) + }) + }) + }) + + describe('with "ignoreMethods" option', function () { + it('should reject invalid value', function () { + assert.throws(createServer.bind(null, { ignoreMethods: 'tj' }), /option ignoreMethods/) + }) + + it('should not check token on given methods', function (done) { + var server = createServer({ ignoreMethods: ['GET', 'POST'] }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var cookie = cookies(res) + request(server) + .post('/') + .set('Cookie', cookie) + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .put('/') + .set('Cookie', cookie) + .expect(403, done) + }) + }) + }) + }) + + describe('with "sessionKey" option', function () { + it('should use the specified sessionKey', function (done) { + var app = connect() + var sess = {} + + app.use(function (req, res, next) { + req.mySession = sess + next() + }) + app.use(bodyParser.urlencoded({ extended: false })) + app.use(csurf({ sessionKey: 'mySession' })) + app.use(function (req, res, next) { + res.end(req.csrfToken() || 'none') + }) + + request(app) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + + request(app) + .post('/') + .send('_csrf=' + encodeURIComponent(token)) + .expect(200, done) + }) + }) + }) + + describe('req.csrfToken()', function () { + it('should return same token for each call', function (done) { + var app = connect() + app.use(session({ keys: ['a', 'b'] })) + app.use(csurf()) + app.use(function (req, res) { + var token1 = req.csrfToken() + var token2 = req.csrfToken() + res.end(String(token1 === token2)) + }) + + request(app) + .get('/') + .expect(200, 'true', done) + }) + + it('should error when secret storage missing', function (done) { + var app = connect() + + app.use(session({ keys: ['a', 'b'] })) + app.use(csurf()) + app.use(function (req, res) { + req.session = null + res.setHeader('x-run', 'true') + res.end(req.csrfToken()) + }) + + request(app) + .get('/') + .expect('x-run', 'true') + .expect(500, /misconfigured csrf/, done) + }) + }) + + describe('when using session storage', function () { + var app + before(function () { + app = connect() + app.use(session({ keys: ['a', 'b'] })) + app.use(csurf()) + app.use('/break', function (req, res, next) { + // break session + req.session = null + next() + }) + app.use('/new', function (req, res, next) { + // regenerate session + req.session = { hit: 1 } + next() + }) + app.use(function (req, res) { + res.end(req.csrfToken() || 'none') + }) + }) + + it('should work with a valid token', function (done) { + request(app) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + request(app) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', token) + .expect(200, done) + }) + }) + + it('should provide a valid token when session regenerated', function (done) { + request(app) + .get('/new') + .expect(200, function (err, res) { + if (err) return done(err) + var token = res.text + request(app) + .post('/') + .set('Cookie', cookies(res)) + .set('X-CSRF-Token', token) + .expect(200, done) + }) + }) + + it('should error if session missing', function (done) { + request(app) + .get('/break') + .expect(500, /misconfigured csrf/, done) + }) + }) +}) + +function cookie (res, name) { + return res.headers['set-cookie'].filter(function (cookies) { + return cookies.split('=')[0] === name + })[0] +} + +function cookies (res) { + return res.headers['set-cookie'].map(function (cookies) { + return cookies.split(';')[0] + }).join(';') +} + +function createServer (opts) { + var app = connect() + + if (!opts || (opts && !opts.cookie)) { + app.use(session({ keys: ['a', 'b'] })) + } else if (opts && opts.cookie) { + app.use(cookieParser('keyboard cat')) + } + + app.use(function (req, res, next) { + var index = req.url.indexOf('?') + 1 + + if (index) { + req.query = querystring.parse(req.url.substring(index)) + } + + next() + }) + app.use(bodyParser.urlencoded({ extended: false })) + app.use(csurf(opts)) + + app.use(function (req, res) { + res.end(req.csrfToken() || 'none') + }) + + return http.createServer(app) +} From ed78305543696039175cc5e09cba04620c361793 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 7 Nov 2024 16:28:54 -0600 Subject: [PATCH 2/3] update readme, bump cookie --- README.md | 4 +++- package.json | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 12a38d1..ca3dc55 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# csurf +# @sailshq/csurf + +A fork of the [now deprecated](https://github.com/expressjs/csurf?tab=readme-ov-file#deprecated) csurf package with ongoing maintenance from the [Sails core team](http://sailsjs.com/about). [![NPM Version][npm-version-image]][npm-url] [![NPM Downloads][npm-downloads-image]][node-url] diff --git a/package.json b/package.json index 7eb22a4..9071298 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "csurf", - "description": "CSRF token middleware", + "description": "A fork of csurf with ongoing maintenance from the Sails core team", "version": "1.11.0", "author": "Jonathan Ong (http://jongleberry.com)", "contributors": [ "Douglas Christopher Wilson " ], "license": "MIT", - "repository": "sailshq/csurf", + "repository": "@sailshq/csurf", "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.6", From bdee25fc5b6f5a89ca65cad01ca83bac33f17a03 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 7 Nov 2024 16:30:17 -0600 Subject: [PATCH 3/3] Update package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9071298..ee5f0c0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "csurf", + "name": "@sailshq/csurf", "description": "A fork of csurf with ongoing maintenance from the Sails core team", "version": "1.11.0", "author": "Jonathan Ong (http://jongleberry.com)", @@ -7,7 +7,7 @@ "Douglas Christopher Wilson " ], "license": "MIT", - "repository": "@sailshq/csurf", + "repository": "sailshq/csurf", "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.6",