From b8fcb06dd2ad73fefad5adbeaf18e1fe0065bd5f Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 10 Jun 2024 10:30:41 +0300 Subject: [PATCH 1/6] added node-http package and example --- examples/express/app.js | 3 - examples/express/package.json | 2 +- examples/node-http/README.md | 21 ++ examples/node-http/package.json | 9 + examples/node-http/server.js | 76 +++++++ packages/express/src/index.test.ts | 57 +---- packages/express/src/index.ts | 28 +-- packages/node-http/README.md | 198 +++++++++++++++++ packages/node-http/package.json | 37 ++++ packages/node-http/src/index.test.ts | 303 +++++++++++++++++++++++++++ packages/node-http/src/index.ts | 116 ++++++++++ packages/node-http/tsconfig.json | 6 + packages/node-http/vite.config.ts | 27 +++ pnpm-lock.yaml | 81 ++++--- 14 files changed, 869 insertions(+), 95 deletions(-) create mode 100644 examples/node-http/README.md create mode 100644 examples/node-http/package.json create mode 100644 examples/node-http/server.js create mode 100644 packages/node-http/README.md create mode 100644 packages/node-http/package.json create mode 100644 packages/node-http/src/index.test.ts create mode 100644 packages/node-http/src/index.ts create mode 100644 packages/node-http/tsconfig.json create mode 100644 packages/node-http/vite.config.ts diff --git a/examples/express/app.js b/examples/express/app.js index 9abb786..e27c7d9 100644 --- a/examples/express/app.js +++ b/examples/express/app.js @@ -12,9 +12,6 @@ const csrfMiddleware = createCsrfMiddleware({ const app = express(); const port = 3000; -// add body parsing middleware -app.use(express.urlencoded({ extended: false })); - // add csrf middleware app.use(csrfMiddleware); diff --git a/examples/express/package.json b/examples/express/package.json index d98f759..24c7c31 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "dependencies": { - "@edge-csrf/express": "^2.1.0", + "@edge-csrf/express": "^2.2.0", "express": "^4.19.2" } } diff --git a/examples/node-http/README.md b/examples/node-http/README.md new file mode 100644 index 0000000..248892e --- /dev/null +++ b/examples/node-http/README.md @@ -0,0 +1,21 @@ +This is an [Express](https://expressjs.com) example app. + +## Getting Started + +First, install dependencies: + +```bash +npm install +# or +pnpm install +# or +yarn install +``` + +Next, run the server: + +```bash +node app.js +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/node-http/package.json b/examples/node-http/package.json new file mode 100644 index 0000000..f693226 --- /dev/null +++ b/examples/node-http/package.json @@ -0,0 +1,9 @@ +{ + "name": "edge-csrf-example", + "version": "0.1.0", + "private": true, + "type": "module", + "dependencies": { + "@edge-csrf/node-http": "^0.0.0" + } +} diff --git a/examples/node-http/server.js b/examples/node-http/server.js new file mode 100644 index 0000000..1d0609f --- /dev/null +++ b/examples/node-http/server.js @@ -0,0 +1,76 @@ +import { createServer } from 'http'; + +import { CsrfError, createCsrfProtect } from '@edge-csrf/node-http'; + +// initalize csrf protection method +const csrfProtect = createCsrfProtect({ + cookie: { + secure: process.env.NODE_ENV === 'production', + }, +}); + +// init server +const server = createServer(async (req, res) => { + // apply csrf protection + try { + await csrfProtect(req, res); + } catch (err) { + if (err instanceof CsrfError) { + res.writeHead(403); + res.end('invalid csrf token'); + return; + } + throw err; + } + + // add handler + if (req.url === '/') { + if (req.method === 'GET') { + const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + +

CSRF token value: ${csrfToken}

+

HTML Form Submission Example:

+
+ Form without CSRF (should fail): + + +
+
+
+ Form with incorrect CSRF (should fail): + + + +
+
+
+ Form with CSRF (should succeed): + + + +
+ + + `); + return; + } + + if (req.method === 'POST') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('success'); + return; + } + } + + res.writeHead(404); + res.end('not found'); +}); + +// start server +server.listen(3000, () => { + console.log('Server is listening on port 3000'); +}); diff --git a/packages/express/src/index.test.ts b/packages/express/src/index.test.ts index dd2c60b..16d3671 100644 --- a/packages/express/src/index.test.ts +++ b/packages/express/src/index.test.ts @@ -7,10 +7,9 @@ import * as util from '@shared/util'; import { ExpressConfig, ExpressTokenOptions, createCsrfMiddleware } from './index'; function createApp(): Express { - const app = express(); - app.use(express.urlencoded({ extended: false })); - const csrfMiddleware = createCsrfMiddleware(); + + const app = express(); app.use(csrfMiddleware); app.get('/', (_, res) => { @@ -24,7 +23,7 @@ function createApp(): Express { return app; } -describe('NextTokenOptions tests', () => { +describe('ExpressTokenOptions tests', () => { it('returns default values when options are absent', () => { const tokenOpts = new ExpressTokenOptions(); expect(tokenOpts.responseHeader).toEqual('X-CSRF-Token'); @@ -42,7 +41,7 @@ describe('NextTokenOptions tests', () => { }); }); -describe('NextConfig tests', () => { +describe('ExpressConfig tests', () => { it('returns default config when options are absent', () => { const config = new ExpressConfig(); expect(config.excludePathPrefixes).toEqual([]); @@ -74,23 +73,11 @@ describe('csrfProtectMiddleware integration tests', () => { expect(token).not.toBe(''); }); - it('should work with express.json()', async () => { - // init app - const app = express(); - app.use(express.json()); - - const csrfMiddleware = createCsrfMiddleware(); - app.use(csrfMiddleware); - - app.post('/', (_, res) => { - res.status(200).json({ success: true }); - }); - - // make request + it('should work with application/json', async () => { const secretUint8 = util.createSecret(8); const tokenUint8 = await util.createToken(secretUint8, 8); - const resp = await request(app) + const resp = await request(testApp) .post('/') .set('Content-Type', 'application/json') .set('Cookie', [`_csrfSecret=${util.utoa(secretUint8)}`]) @@ -103,23 +90,11 @@ describe('csrfProtectMiddleware integration tests', () => { expect(newTokenStr).not.toBe(''); }); - it('should work with express.text()', async () => { - // init app - const app = express(); - app.use(express.text()); - - const csrfMiddleware = createCsrfMiddleware(); - app.use(csrfMiddleware); - - app.post('/', (_, res) => { - res.status(200).json({ success: true }); - }); - - // make request + it('should work with text/plain', async () => { const secretUint8 = util.createSecret(8); const tokenUint8 = await util.createToken(secretUint8, 8); - const resp = await request(app) + const resp = await request(testApp) .post('/') .set('Content-Type', 'text/plain') .set('Cookie', [`_csrfSecret=${util.utoa(secretUint8)}`]) @@ -132,23 +107,11 @@ describe('csrfProtectMiddleware integration tests', () => { expect(newTokenStr).not.toBe(''); }); - it('should work with express.urlencoded()', async () => { - // init app - const app = express(); - app.use(express.urlencoded({ extended: false })); - - const csrfMiddleware = createCsrfMiddleware(); - app.use(csrfMiddleware); - - app.post('/', (_, res) => { - res.status(200).json({ success: true }); - }); - - // make request + it('should work with application/x-www-form-urlencoded', async () => { const secretUint8 = util.createSecret(8); const tokenUint8 = await util.createToken(secretUint8, 8); - const resp = await request(app) + const resp = await request(testApp) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .set('Cookie', [`_csrfSecret=${util.utoa(secretUint8)}`]) diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index bc2d843..d391e3d 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -6,6 +6,20 @@ import type { ConfigOptions } from '@shared/protect'; export { CsrfError }; +/** + * Parse request body as string + * @param {ExpressRequest} req - The node http request + * @returns Promise that resolves to the body + */ +function getRequestBody(req: ExpressRequest): Promise { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', (chunk) => body += chunk.toString()); + req.on('end', () => resolve(body)); + req.on('error', (err) => reject(err)); + }); +} + /** * Represents token options in config */ @@ -72,23 +86,11 @@ export function createCsrfProtect(opts?: Partial): Express else if (value !== undefined) headers.append(key, value); }); - let body: URLSearchParams | string | undefined; - if (!['GET', 'HEAD'].includes(req.method)) { - const contentType = headers.get('content-type') || 'text/plain'; - if (typeof req.body === 'string') { - body = req.body; - } else if (typeof req.body === 'object' && ['application/json', 'application/ld+json'].includes(contentType)) { - body = JSON.stringify(req.body); - } else { - body = new URLSearchParams(req.body); - } - } - // init request object const request = new Request(url, { method: req.method, headers, - body, + body: ['GET', 'HEAD'].includes(req.method || '') ? undefined : await getRequestBody(req), }); // execute protect function diff --git a/packages/node-http/README.md b/packages/node-http/README.md new file mode 100644 index 0000000..082d207 --- /dev/null +++ b/packages/node-http/README.md @@ -0,0 +1,198 @@ +# Express + +This is the documentation for Edge-CSRF's Express integration. + +## Quickstart + +First, add the integration library as a dependency: + +```console +npm install @edge-csrf/express +# or +pnpm add @edge-csrf/express +# or +yarn add @edge-csrf/express +``` + +Next, add the Edge-CSRF middleware to your app: + +```javascript +// app.js + +import { createCsrfMiddleware } from '@edge-csrf/express'; +import express from 'express'; + +// initalize csrf protection middleware +const csrfMiddleware = createCsrfMiddleware({ + cookie: { + secure: process.env.NODE_ENV === 'production', + }, +}); + +// init app +const app = express(); +const port = 3000; + +// add body parsing middleware +app.use(express.urlencoded({ extended: false })); + +// add csrf middleware +app.use(csrfMiddleware); + +// define handlers +app.get('/', (_, res) => { + res.status(200).json({ success: true }); +}); + +// start server +app.listen(port, () => { + console.log(`Example app listening on port ${port}`) +}); +``` + +Now, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. To add the CSRF token to your forms, you can fetch it from the `X-CSRF-Token` HTTP response header server-side or client-side. For example: + +```javascript +// app.js +... + +// define handlers +app.get('/my-form', (req, res) => { + const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; + res.send(` + + + +

CSRF token value: ${csrfToken}

+
+ Form with CSRF (should succeed): + + + +
+ + + `); +}); + +app.post('/my-form', (req, res) => { + res.send('success'); +}); + +... +``` + +## Example + +Check out the example Express app in this repository: [Express example](examples/express). + +## Lower-level implementations + +If you want lower-level control over the response or which routes CSRF protection will be applied to you can use the `createCsrfProtect()` method to create a function that you can use inside your own custom middleware: + +```typescript +// app.js + +import { CsrfError, createCsrfProtect } from '@edge-csrf/express'; +import express from 'express'; + +// initalize csrf protection method +const csrfProtect = createCsrfProtect({ + cookie: { + secure: process.env.NODE_ENV === 'production', + }, +}); + +// init app +const app = express(); +const port = 3000; + +// add body parsing middleware +app.use(express.urlencoded({ extended: false })); + +// add csrf middleware +app.use(async (req, res, next) => { + try { + await csrfProtect(req, res) + } catch (err) { + if (err instanceof CsrfError) { + res.statusCode = 403; + res.send('invalid csrf token'); + res.end(); + return; + } + throw err; + } +}); + +// define handlers +app.get('/', (_, res) => { + res.status(200).json({ success: true }); +}); + +// start server +app.listen(port, () => { + console.log(`Example app listening on port ${port}`) +}); +``` + +## Configuration + +```javascript +// default config + +{ + cookie: { + name: '_csrfSecret', + path: '/', + maxAge: undefined, + domain: '', + secure: true, + httpOnly: true, + sameSite: 'strict' + }, + excludePathPrefixes: [], + ignoreMethods: ['GET', 'HEAD', 'OPTIONS'], + saltByteLength: 8, + secretByteLength: 18, + token: { + responseHeader: 'X-CSRF-Token' + } +} +``` + +## API + +The following are named exports in the the `@edge-csrf/express` module: + +### Types + +``` +ExpressCsrfProtect - A function that implements CSRF protection for Express requests + + * @param {Request} request - The Express request instance + * @param {Response} response - The Express response instance + * @returns {Promise} - The function completed successfully + * @throws {CsrfError} - The function encountered a CSRF error +``` + +### Classes + +``` +CsrfError - A class that inherits from Error and represents CSRF errors +``` + +### Methods + +``` +createCsrfMiddleware([, options]) - Create a new instance of Express middleware + + * @param {object} options - The configuration options + * @returns {ReqestHandler} - The middleware + +createCsrfProtect([, options]) - Create a lower-level function that can be used inside Express middleware + to implement CSRF protection for requests + + * @param {object} options - The configuration options + * @returns {ExpressCsrfProtect} - The CSRF protection function +``` diff --git a/packages/node-http/package.json b/packages/node-http/package.json new file mode 100644 index 0000000..32b16a8 --- /dev/null +++ b/packages/node-http/package.json @@ -0,0 +1,37 @@ +{ + "name": "@edge-csrf/node-http", + "version": "0.0.0", + "description": "Edge-CSRF integration library for node's http module", + "author": "Andres Morey", + "license": "MIT", + "repository": "kubetail-org/edge-csrf", + "type": "module", + "sideEffects": false, + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "build": "tsc && vite build", + "lint": "eslint \"./src/**/*.ts{,x}\"", + "test": "vitest", + "test-all": "vitest run --environment node && vitest run --environment edge-runtime && vitest run --environment miniflare" + }, + "keywords": [ + "csrf", + "tokens", + "edge", + "node", + "createServer" + ], + "dependencies": { + "cookie": "^0.6.0" + }, + "devDependencies": { + "@types/cookie": "^0.6.0", + "@types/supertest": "^6.0.2", + "supertest": "^7.0.0" + } +} diff --git a/packages/node-http/src/index.test.ts b/packages/node-http/src/index.test.ts new file mode 100644 index 0000000..c5bc8d9 --- /dev/null +++ b/packages/node-http/src/index.test.ts @@ -0,0 +1,303 @@ +import { createServer } from 'http'; + +import request from 'supertest'; + +import * as util from '@shared/util'; + +import { CsrfError, createCsrfProtect, NodeHttpConfig, NodeHttpTokenOptions } from './index'; + +function createApp() { + const csrfProtect = createCsrfProtect(); + + return createServer(async (req, res) => { + // apply csrf protection + try { + await csrfProtect(req, res); + } catch (err) { + if (err instanceof CsrfError) { + res.writeHead(403); + res.end('invalid csrf token'); + return; + } + throw err; + } + + if (req.url === '/' && ['GET', 'POST'].includes(req.method || '')) { + res.writeHead(200); + res.end('ok'); + } else { + res.writeHead(404); + res.end('not found'); + } + }); +} + +describe('NodeHttpTokenOptions tests', () => { + it('returns default values when options are absent', () => { + const tokenOpts = new NodeHttpTokenOptions(); + expect(tokenOpts.responseHeader).toEqual('X-CSRF-Token'); + }); + + it('handles overrides', () => { + const tokenOpts = new NodeHttpTokenOptions({ responseHeader: 'XXX' }); + expect(tokenOpts.responseHeader).toEqual('XXX'); + }); + + it('handles overrides of parent attributes', () => { + const fn = async () => ''; + const tokenOpts = new NodeHttpTokenOptions({ value: fn }); + expect(tokenOpts.value).toBe(fn); + }); +}); + +describe('NodeHttpConfig tests', () => { + it('returns default config when options are absent', () => { + const config = new NodeHttpConfig(); + expect(config.excludePathPrefixes).toEqual([]); + expect(config.token instanceof NodeHttpTokenOptions).toBe(true); + }); + + it('handles top-level overrides', () => { + const config = new NodeHttpConfig({ excludePathPrefixes: ['/xxx/'] }); + expect(config.excludePathPrefixes).toEqual(['/xxx/']); + }); + + it('handles nested token overrides', () => { + const config = new NodeHttpConfig({ token: { responseHeader: 'XXX' } }); + expect(config.token.responseHeader).toEqual('XXX'); + }); +}); + +describe('csrfProtect integration tests', async () => { + const testApp = createApp(); + + it('adds token to response header', async () => { + const resp = await request(testApp) + .get('/') + .expect(200); + + // assertions + const token = resp.header['x-csrf-token']; + expect(token).toBeDefined(); + expect(token).not.toBe(''); + }); + + it('should work with application/json', async () => { + const secretUint8 = util.createSecret(8); + const tokenUint8 = await util.createToken(secretUint8, 8); + + const resp = await request(testApp) + .post('/') + .set('Content-Type', 'application/json') + .set('Cookie', [`_csrfSecret=${util.utoa(secretUint8)}`]) + .send(JSON.stringify({ csrf_token: util.utoa(tokenUint8) })) + .expect(200); + + // assertions + const newTokenStr = resp.headers['x-csrf-token']; + expect(newTokenStr).toBeDefined(); + expect(newTokenStr).not.toBe(''); + }); + + it('should work with text/plain', async () => { + const secretUint8 = util.createSecret(8); + const tokenUint8 = await util.createToken(secretUint8, 8); + + const resp = await request(testApp) + .post('/') + .set('Content-Type', 'text/plain') + .set('Cookie', [`_csrfSecret=${util.utoa(secretUint8)}`]) + .send(util.utoa(tokenUint8)) + .expect(200); + + // assertions + const newTokenStr = resp.headers['x-csrf-token']; + expect(newTokenStr).toBeDefined(); + expect(newTokenStr).not.toBe(''); + }); + + it('should work with application/x-www-form-urlencoded', async () => { + const secretUint8 = util.createSecret(8); + const tokenUint8 = await util.createToken(secretUint8, 8); + + const resp = await request(testApp) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .set('Cookie', [`_csrfSecret=${util.utoa(secretUint8)}`]) + .send(`csrf_token=${encodeURIComponent(util.utoa(tokenUint8))}`) + .expect(200); + + // assertions + const newTokenStr = resp.headers['x-csrf-token']; + expect(newTokenStr).toBeDefined(); + expect(newTokenStr).not.toBe(''); + }); + + it('should work in x-csrf-token header', async () => { + const secret = util.createSecret(8); + const token = await util.createToken(secret, 8); + + const resp = await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${util.utoa(secret)}`]) + .set('X-CSRF-Token', util.utoa(token)) + .expect(200); + + // assertions + const newTokenStr = resp.headers['x-csrf-token']; + expect(newTokenStr).toBeDefined(); + expect(newTokenStr).not.toBe(''); + }); + + it('should reject token from different secret', async () => { + const goodSecret = util.createSecret(8); + const evilSecret = util.createSecret(8); + const evilToken = await util.createToken(evilSecret, 8); + + await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${util.utoa(goodSecret)}`]) + .set('X-CSRF-Token', util.utoa(evilToken)) + .expect(403); + }); + + it('should reject invalid token', async () => { + const secret = util.createSecret(8); + + await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${util.utoa(secret)}`]) + .set('X-CSRF-Token', btoa(String.fromCharCode(100))) + .expect(403); + }); + + it('should reject non-base64 token', async () => { + const secret = util.createSecret(8); + + await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${util.utoa(secret)}`]) + .set('X-CSRF-Token', '-') + .expect(403); + }); + + it('should reject no token', async () => { + const secret = util.createSecret(8); + + await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${util.utoa(secret)}`]) + .expect(403); + }); + + it('should reject empty token', async () => { + const secret = util.createSecret(8); + + await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${util.utoa(secret)}`]) + .set('X-CSRF-Token', '') + .expect(403); + }); + + it('should reject with non-base64 secret', async () => { + const secret = util.createSecret(8); + const token = await util.createToken(secret, 8); + + await request(testApp) + .post('/') + .set('Cookie', ['_csrfSecret=-']) + .set('X-CSRF-Token', util.utoa(token)) + .expect(403); + }); + + it('should reject invalid secret', async () => { + const secret = util.createSecret(8); + const token = await util.createToken(secret, 8); + + await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${btoa(String.fromCharCode(100))}`]) + .set('X-CSRF-Token', util.utoa(token)) + .expect(403); + }); + + it('should reject no secret', async () => { + const secret = util.createSecret(8); + const token = await util.createToken(secret, 8); + + await request(testApp) + .post('/') + .set('X-CSRF-Token', util.utoa(token)) + .expect(403); + }); + + it('should reject empty secret', async () => { + const secret = util.createSecret(8); + const token = await util.createToken(secret, 8); + + await request(testApp) + .post('/') + .set('Cookie', ['_csrfSecret=']) + .set('X-CSRF-Token', util.utoa(token)) + .expect(403); + }); +}); + +describe('obtaining secrets tests', () => { + const testApp = createApp(); + + describe('sets new secret when missing from request', () => { + it('GET', async () => { + const resp = await request(testApp).get('/'); + const setCookieList = resp.get('Set-Cookie'); + expect(setCookieList).not.toBeUndefined(); + if (!setCookieList) return; + expect(setCookieList?.length).toEqual(1); + expect(setCookieList[0].startsWith('_csrfSecret=')).toEqual(true); + }); + + it('POST', async () => { + const resp = await request(testApp).post('/'); + const setCookieList = resp.get('Set-Cookie'); + expect(setCookieList).not.toBeUndefined(); + if (!setCookieList) return; + expect(setCookieList?.length).toEqual(1); + expect(setCookieList[0].startsWith('_csrfSecret=')).toEqual(true); + }); + }); + + describe('keeps existing secret when present in request', () => { + const secretStr = util.utoa(util.createSecret(8)); + + it('GET', async () => { + const resp = await request(testApp) + .get('/') + .set('Cookie', [`_csrfSecret=${secretStr}`]); + expect(resp.get('Set-Cookie')).toBeUndefined(); + }); + + it('POST', async () => { + const resp = await request(testApp) + .post('/') + .set('Cookie', [`_csrfSecret=${secretStr}`]); + expect(resp.get('Set-Cookie')).toBeUndefined(); + }); + }); + + it('creates unique secret on subsequent empty request', async () => { + // 1st request + const resp1 = await request(testApp).get('/'); + const setCookie1 = resp1.get('Set-Cookie'); + + // 2nd request + const resp2 = await request(testApp).get('/'); + const setCookie2 = resp2.get('Set-Cookie'); + + // compare secrets + expect(setCookie1).not.toEqual(undefined); + expect(setCookie2).not.toEqual(undefined); + if (!setCookie1 || !setCookie2) return; + expect(setCookie1[0]).not.toEqual(setCookie2[0]); + }); +}); diff --git a/packages/node-http/src/index.ts b/packages/node-http/src/index.ts new file mode 100644 index 0000000..8477348 --- /dev/null +++ b/packages/node-http/src/index.ts @@ -0,0 +1,116 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + +import * as cookielib from 'cookie'; + +import { CsrfError, createCsrfProtect as _createCsrfProtect, Config, TokenOptions } from '@shared/protect'; +import type { ConfigOptions } from '@shared/protect'; + +export { CsrfError }; + +/** + * Parse request body as string + * @param {IncomingMessage} req - The node http request + * @returns Promise that resolves to the body + */ +function getRequestBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', (chunk) => body += chunk.toString()); + req.on('end', () => resolve(body)); + req.on('error', (err) => reject(err)); + }); +} + +/** + * Represents token options in config + */ +export class NodeHttpTokenOptions extends TokenOptions { + responseHeader: string = 'X-CSRF-Token'; + + constructor(opts?: Partial) { + super(opts); + Object.assign(this, opts); + } +} + +/** + * Represents configuration object + */ +export class NodeHttpConfig extends Config { + excludePathPrefixes: string[] = []; + + token: NodeHttpTokenOptions = new NodeHttpTokenOptions(); + + constructor(opts?: Partial) { + super(opts); + const newOpts = opts || {}; + if (newOpts.token) newOpts.token = new NodeHttpTokenOptions(newOpts.token); + Object.assign(this, newOpts); + } +} + +/** + * Represents configuration options object + */ +export interface NodeHttpConfigOptions extends Omit { + token: Partial; +} + +/** + * Represents signature of CSRF protect function to be used in node-http request handlers + */ +export type NodeHttpCsrfProtect = { + (request: IncomingMessage, response: ServerResponse): Promise; +}; + +/** + * Create CSRF protection function for use in node-http request handlers + * @param {Partial} opts - Configuration options + * @returns {NodeHttpCsrfProtect} - The CSRF protect function + * @throws {CsrfError} - An error if CSRF validation failed + */ +export function createCsrfProtect(opts?: Partial): NodeHttpCsrfProtect { + const config = new NodeHttpConfig(opts); + const _csrfProtect = _createCsrfProtect(config); + + return async (req, res) => { + // parse cookies + const cookies = cookielib.parse(req.headers.cookie || ''); + + // init url + const host = req.headers.host; + const originalUrl = req.url || ''; + const url = new URL(`http://${host}${originalUrl}`); + + // init headers + const headers = new Headers(); + Object.entries(req.headers).forEach(([key, value]) => { + if (Array.isArray(value)) value.forEach((val) => headers.append(key, val)); + else if (value !== undefined) headers.append(key, value); + }); + + // init request object + const request = new Request(url, { + method: req.method, + headers, + body: ['GET', 'HEAD'].includes(req.method || '') ? undefined : await getRequestBody(req), + }); + + // execute protect function + const token = await _csrfProtect({ + request, + url, + getCookie: (name) => cookies[name], + setCookie: (cookie) => { + const newCookie = cookielib.serialize(cookie.name, cookie.value, cookie); + const existingCookies = res.getHeader('Set-Cookie'); + if (Array.isArray(existingCookies)) res.setHeader('Set-Cookie', [...existingCookies, newCookie]); + else if (typeof existingCookies === 'string') res.setHeader('Set-Cookie', [existingCookies, newCookie]); + else res.setHeader('Set-Cookie', newCookie); + }, + }); + + // add token to response header + if (token) res.setHeader(config.token.responseHeader, token); + }; +} diff --git a/packages/node-http/tsconfig.json b/packages/node-http/tsconfig.json new file mode 100644 index 0000000..47a1b59 --- /dev/null +++ b/packages/node-http/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/node-http/vite.config.ts b/packages/node-http/vite.config.ts new file mode 100644 index 0000000..2831692 --- /dev/null +++ b/packages/node-http/vite.config.ts @@ -0,0 +1,27 @@ +/// +import { resolve } from 'path'; +import { defineConfig } from 'vite'; + +import dts from '../../shared/src/vite-plugin-dts'; + +export default defineConfig({ + resolve: { + alias: { + '@shared': resolve(__dirname, '../../shared/src'), + }, + }, + plugins: [dts()], + build: { + lib: { + entry: [ + resolve(__dirname, 'src/index.ts'), + ], + name: '@edge-csrf/node-http', + formats: ['es', 'cjs'], + } + }, + test: { + environment: 'edge-runtime', + globals: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28c1808..64cbdab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,14 +48,11 @@ importers: specifier: ^2.14.2 version: 2.14.2(vitest@1.5.0(@edge-runtime/vm@3.2.0)(@types/node@20.12.7)) - examples/express: + examples/node-http: dependencies: - '@edge-csrf/express': - specifier: 2.1.0-rc1 - version: 2.1.0-rc1(express@4.19.2) - express: - specifier: ^4.19.2 - version: 4.19.2 + '@edge-csrf/node-http': + specifier: ^0.0.0 + version: link:../../packages/node-http packages/core: {} @@ -87,11 +84,27 @@ importers: specifier: ^14.2.0 version: 14.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + packages/node-http: + dependencies: + cookie: + specifier: ^0.6.0 + version: 0.6.0 + devDependencies: + '@types/cookie': + specifier: ^0.6.0 + version: 0.6.0 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.2 + supertest: + specifier: ^7.0.0 + version: 7.0.0 + packages/sveltekit: devDependencies: '@sveltejs/kit': specifier: ^2.5.6 - version: 2.5.6(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)) + version: 2.5.6(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)) shared: {} @@ -120,11 +133,6 @@ packages: '@cloudflare/workers-types@4.20240419.0': resolution: {integrity: sha512-UM16sr4HEe0mDj6C5OFcodzdj/CnEp0bfncAq3g7OpDsoZ1sBrfsMrb7Yc4f8J81EemvmQZyE6sSanpURtVkcQ==} - '@edge-csrf/express@2.1.0-rc1': - resolution: {integrity: sha512-LHEe3WhOAcGSASbuoGDDJxcyYJFw37XToOp9/KTSPL6mmnYiDLcKKSKgO09XLnec3y/UfgtHgO1v+O6GLKL8Cw==} - peerDependencies: - express: ^4.0.0 - '@edge-runtime/primitives@4.1.0': resolution: {integrity: sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==} engines: {node: '>=16'} @@ -786,6 +794,9 @@ packages: '@types/node@20.12.7': resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + '@types/node@20.14.2': + resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} + '@types/qs@6.9.15': resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} @@ -2726,11 +2737,6 @@ snapshots: '@cloudflare/workers-types@4.20240419.0': {} - '@edge-csrf/express@2.1.0-rc1(express@4.19.2)': - dependencies: - cookie: 0.6.0 - express: 4.19.2 - '@edge-runtime/primitives@4.1.0': {} '@edge-runtime/vm@3.2.0': @@ -3165,9 +3171,9 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sveltejs/kit@2.5.6(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7))': + '@sveltejs/kit@2.5.6(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)) + '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 4.3.3 @@ -3181,28 +3187,28 @@ snapshots: sirv: 2.0.4 svelte: 4.2.15 tiny-glob: 0.2.9 - vite: 5.2.9(@types/node@20.12.7) + vite: 5.2.9(@types/node@20.14.2) - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7))': + '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)) + '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)) debug: 4.3.4 svelte: 4.2.15 - vite: 5.2.9(@types/node@20.12.7) + vite: 5.2.9(@types/node@20.14.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7))': + '@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.12.7)) + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)))(svelte@4.2.15)(vite@5.2.9(@types/node@20.14.2)) debug: 4.3.4 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.10 svelte: 4.2.15 svelte-hmr: 0.16.0(svelte@4.2.15) - vite: 5.2.9(@types/node@20.12.7) - vitefu: 0.2.5(vite@5.2.9(@types/node@20.12.7)) + vite: 5.2.9(@types/node@20.14.2) + vitefu: 0.2.5(vite@5.2.9(@types/node@20.14.2)) transitivePeerDependencies: - supports-color @@ -3275,6 +3281,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@20.14.2': + dependencies: + undici-types: 5.26.5 + '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} @@ -3298,7 +3308,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 20.12.7 + '@types/node': 20.14.2 '@types/supertest@6.0.2': dependencies: @@ -5361,9 +5371,18 @@ snapshots: '@types/node': 20.12.7 fsevents: 2.3.3 - vitefu@0.2.5(vite@5.2.9(@types/node@20.12.7)): + vite@5.2.9(@types/node@20.14.2): + dependencies: + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.14.3 optionalDependencies: - vite: 5.2.9(@types/node@20.12.7) + '@types/node': 20.14.2 + fsevents: 2.3.3 + + vitefu@0.2.5(vite@5.2.9(@types/node@20.14.2)): + optionalDependencies: + vite: 5.2.9(@types/node@20.14.2) vitest-environment-miniflare@2.14.2(vitest@1.5.0(@edge-runtime/vm@3.2.0)(@types/node@20.12.7)): dependencies: From f5dcfb320aa247dfe8bcdb4cc8f68dfa2015a9dc Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 10 Jun 2024 10:46:28 +0300 Subject: [PATCH 2/6] wip --- README.md | 115 ++++++++++++++++++----- packages/express/README.md | 6 -- packages/node-http/README.md | 173 ++++++++++++----------------------- 3 files changed, 151 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index b544d77..0588e47 100644 --- a/README.md +++ b/README.md @@ -198,38 +198,18 @@ const csrfMiddleware = createCsrfMiddleware({ const app = express(); const port = 3000; -// add body parsing middleware -app.use(express.urlencoded({ extended: false })); - // add csrf middleware app.use(csrfMiddleware); // define handlers -app.get('/', (_, res) => { - res.status(200).json({ success: true }); -}); - -// start server -app.listen(port, () => { - console.log(`Example app listening on port ${port}`) -}); -``` - -Now, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. To add the CSRF token to your forms, you can fetch it from the `X-CSRF-Token` HTTP response header server-side or client-side. For example: - -```javascript -// app.js -... - -// define handlers -app.get('/my-form', (req, res) => { +app.get('/', (req, res) => { const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; res.send(`

CSRF token value: ${csrfToken}

-
+ Form with CSRF (should succeed): @@ -240,13 +220,100 @@ app.get('/my-form', (req, res) => { `); }); -app.post('/my-form', (req, res) => { +app.post('/', (req, res) => { res.send('success'); }); -... +// start server +app.listen(port, () => { + console.log(`Example app listening on port ${port}`) +}); ``` +With the middleware installed, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. + +## Quickstart (Node-HTTP) + +First, install Edge-CSRF's Node-HTTP integration library: + +```console +npm install @edge-csrf/node-http +# or +pnpm add @edge-csrf/node-http +# or +yarn add @edge-csrf/node-http +``` + +Next, add the Edge-CSRF CSRF protection function to your request handlers: + +```javascript +// server.js + +import { createServer } from 'http'; + +import { createCsrfProtect } from '@edge-csrf/node-http'; + +// initalize csrf protection middleware +const csrfProtect = createCsrfProtect({ + cookie: { + secure: process.env.NODE_ENV === 'production', + }, +}); + +// init server +const server = createServer(async (req, res) => { + // apply csrf protection + try { + await csrfProtect(req, res); + } catch (err) { + if (err instanceof CsrfError) { + res.writeHead(403); + res.end('invalid csrf token'); + return; + } + throw err; + } + + // add handler + if (req.url === '/') { + if (req.method === 'GET') { + const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + + + Form with CSRF (should succeed): + + + +
+ + + `); + return; + } + + if (req.method === 'POST') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('success'); + return; + } + } + + res.writeHead(404); + res.end('not found'); +}); + +// start server +server.listen(3000, () => { + console.log('Server is listening on port 3000'); +}); +``` + +With the CSRF protection method, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. + ## Development ### Get the code diff --git a/packages/express/README.md b/packages/express/README.md index 082d207..a7dff06 100644 --- a/packages/express/README.md +++ b/packages/express/README.md @@ -33,9 +33,6 @@ const csrfMiddleware = createCsrfMiddleware({ const app = express(); const port = 3000; -// add body parsing middleware -app.use(express.urlencoded({ extended: false })); - // add csrf middleware app.use(csrfMiddleware); @@ -107,9 +104,6 @@ const csrfProtect = createCsrfProtect({ const app = express(); const port = 3000; -// add body parsing middleware -app.use(express.urlencoded({ extended: false })); - // add csrf middleware app.use(async (req, res, next) => { try { diff --git a/packages/node-http/README.md b/packages/node-http/README.md index 082d207..b1b71b5 100644 --- a/packages/node-http/README.md +++ b/packages/node-http/README.md @@ -1,141 +1,93 @@ -# Express +# Node-HTTP -This is the documentation for Edge-CSRF's Express integration. +This is the documentation for Edge-CSRF's Node `http` module integration. ## Quickstart First, add the integration library as a dependency: ```console -npm install @edge-csrf/express +npm install @edge-csrf/node-http # or -pnpm add @edge-csrf/express +pnpm add @edge-csrf/node-http # or -yarn add @edge-csrf/express +yarn add @edge-csrf/node-http ``` -Next, add the Edge-CSRF middleware to your app: +Next, add the Edge-CSRF CSRF protection function to your app: ```javascript -// app.js +// server.js -import { createCsrfMiddleware } from '@edge-csrf/express'; -import express from 'express'; +import { createServer } from 'http'; -// initalize csrf protection middleware -const csrfMiddleware = createCsrfMiddleware({ - cookie: { - secure: process.env.NODE_ENV === 'production', - }, -}); - -// init app -const app = express(); -const port = 3000; - -// add body parsing middleware -app.use(express.urlencoded({ extended: false })); - -// add csrf middleware -app.use(csrfMiddleware); - -// define handlers -app.get('/', (_, res) => { - res.status(200).json({ success: true }); -}); - -// start server -app.listen(port, () => { - console.log(`Example app listening on port ${port}`) -}); -``` - -Now, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. To add the CSRF token to your forms, you can fetch it from the `X-CSRF-Token` HTTP response header server-side or client-side. For example: - -```javascript -// app.js -... - -// define handlers -app.get('/my-form', (req, res) => { - const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; - res.send(` - - - -

CSRF token value: ${csrfToken}

-
- Form with CSRF (should succeed): - - - -
- - - `); -}); - -app.post('/my-form', (req, res) => { - res.send('success'); -}); - -... -``` - -## Example +import { createCsrfProtect } from '@edge-csrf/node-http'; -Check out the example Express app in this repository: [Express example](examples/express). - -## Lower-level implementations - -If you want lower-level control over the response or which routes CSRF protection will be applied to you can use the `createCsrfProtect()` method to create a function that you can use inside your own custom middleware: - -```typescript -// app.js - -import { CsrfError, createCsrfProtect } from '@edge-csrf/express'; -import express from 'express'; - -// initalize csrf protection method +// initalize csrf protection middleware const csrfProtect = createCsrfProtect({ cookie: { secure: process.env.NODE_ENV === 'production', }, }); -// init app -const app = express(); -const port = 3000; - -// add body parsing middleware -app.use(express.urlencoded({ extended: false })); - -// add csrf middleware -app.use(async (req, res, next) => { +// init server +const server = createServer(async (req, res) => { + // apply csrf protection try { - await csrfProtect(req, res) + await csrfProtect(req, res); } catch (err) { if (err instanceof CsrfError) { - res.statusCode = 403; - res.send('invalid csrf token'); - res.end(); + res.writeHead(403); + res.end('invalid csrf token'); return; } throw err; } -}); -// define handlers -app.get('/', (_, res) => { - res.status(200).json({ success: true }); + // add handler + if (req.url === '/') { + if (req.method === 'GET') { + const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + +
+ Form with CSRF (should succeed): + + + +
+ + + `); + return; + } + + if (req.method === 'POST') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('success'); + return; + } + } + + res.writeHead(404); + res.end('not found'); }); // start server -app.listen(port, () => { - console.log(`Example app listening on port ${port}`) +server.listen(3000, () => { + console.log('Server is listening on port 3000'); }); ``` +With the CSRF protection method, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. + +## Example + +Check out the example Node-HTTP server in this repository: [Node-HTTP example](examples/node-http). + ## Configuration ```javascript @@ -163,15 +115,15 @@ app.listen(port, () => { ## API -The following are named exports in the the `@edge-csrf/express` module: +The following are named exports in the the `@edge-csrf/node-http` module: ### Types ``` -ExpressCsrfProtect - A function that implements CSRF protection for Express requests +NodeHttpCsrfProtect - A function that implements CSRF protection for Node http requests - * @param {Request} request - The Express request instance - * @param {Response} response - The Express response instance + * @param {IncomingMessage} request - The Node HTTP module request instance + * @param {ServerResponse} response - The Node HTTP module response instance * @returns {Promise} - The function completed successfully * @throws {CsrfError} - The function encountered a CSRF error ``` @@ -185,14 +137,9 @@ CsrfError - A class that inherits from Error and represents CSRF errors ### Methods ``` -createCsrfMiddleware([, options]) - Create a new instance of Express middleware - - * @param {object} options - The configuration options - * @returns {ReqestHandler} - The middleware - -createCsrfProtect([, options]) - Create a lower-level function that can be used inside Express middleware +createCsrfProtect([, options]) - Create a function that can be used inside Node HTTP handlers to implement CSRF protection for requests * @param {object} options - The configuration options - * @returns {ExpressCsrfProtect} - The CSRF protection function + * @returns {NodeHttpCsrfProtect} - The CSRF protection function ``` From d019e69d2597e2bbb98798174e0ee05714d11ae5 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 10 Jun 2024 10:47:32 +0300 Subject: [PATCH 3/6] wip --- README.md | 3 ++- examples/node-http/package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0588e47..616e40c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ We hope you enjoy using this software. Contributions and suggestions are welcome ## Features - Runs on both node and edge runtimes -- Includes integrations for [Next.js](packages/nextjs), [Sveltekit](packages/sveltekit) and [Express](packages/express) +- Includes integrations for [Next.js](packages/nextjs), [Sveltekit](packages/sveltekit), [Express](packages/express) and [Node-HTTP](packages/node-http) - Includes a low-level API for custom integrations ([see here](packages/core)) - Handles form-urlencoded, multipart/form-data or json-encoded HTTP request bodies - Gets token from HTTP request header or from request body @@ -21,6 +21,7 @@ We hope you enjoy using this software. Contributions and suggestions are welcome * [Next.js](packages/nextjs) * [SvelteKit](packages/sveltekit) * [Express](packages/express) +* [Node-HTTP](packages/node-http) * [Core API](packages/core) ## Quickstart (Next.js) diff --git a/examples/node-http/package.json b/examples/node-http/package.json index f693226..81060c0 100644 --- a/examples/node-http/package.json +++ b/examples/node-http/package.json @@ -4,6 +4,6 @@ "private": true, "type": "module", "dependencies": { - "@edge-csrf/node-http": "^0.0.0" + "@edge-csrf/node-http": "^2.2.0" } } From df07cf2b0a1d9d535adbce2fe03308548da5ac51 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 10 Jun 2024 10:53:23 +0300 Subject: [PATCH 4/6] wip --- packages/node-http/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-http/README.md b/packages/node-http/README.md index b1b71b5..d088544 100644 --- a/packages/node-http/README.md +++ b/packages/node-http/README.md @@ -1,6 +1,6 @@ # Node-HTTP -This is the documentation for Edge-CSRF's Node `http` module integration. +This is the documentation for Edge-CSRF's Node built-in http module integration. ## Quickstart From 839c55e20db03f78bbaa72889df007ad6a9fd25c Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 10 Jun 2024 12:27:10 +0300 Subject: [PATCH 5/6] wip --- packages/node-http/src/index.test.ts | 2 +- packages/node-http/src/index.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/node-http/src/index.test.ts b/packages/node-http/src/index.test.ts index c5bc8d9..0de502f 100644 --- a/packages/node-http/src/index.test.ts +++ b/packages/node-http/src/index.test.ts @@ -27,7 +27,7 @@ function createApp() { res.end('ok'); } else { res.writeHead(404); - res.end('not found'); + res.end('not found'); } }); } diff --git a/packages/node-http/src/index.ts b/packages/node-http/src/index.ts index 8477348..16ac33a 100644 --- a/packages/node-http/src/index.ts +++ b/packages/node-http/src/index.ts @@ -15,7 +15,7 @@ export { CsrfError }; function getRequestBody(req: IncomingMessage): Promise { return new Promise((resolve, reject) => { let body = ''; - req.on('data', (chunk) => body += chunk.toString()); + req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => resolve(body)); req.on('error', (err) => reject(err)); }); @@ -78,9 +78,8 @@ export function createCsrfProtect(opts?: Partial): NodeHt const cookies = cookielib.parse(req.headers.cookie || ''); // init url - const host = req.headers.host; - const originalUrl = req.url || ''; - const url = new URL(`http://${host}${originalUrl}`); + const { url: originalUrl, headers: { host } } = req; + const url = new URL(`http://${host}${originalUrl || ''}`); // init headers const headers = new Headers(); From deb35e8acfd980431880f7ca3e10892d457d0ab2 Mon Sep 17 00:00:00 2001 From: Andres Morey Date: Mon, 10 Jun 2024 12:29:51 +0300 Subject: [PATCH 6/6] wip --- packages/express/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index d391e3d..6a40686 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -14,7 +14,7 @@ export { CsrfError }; function getRequestBody(req: ExpressRequest): Promise { return new Promise((resolve, reject) => { let body = ''; - req.on('data', (chunk) => body += chunk.toString()); + req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => resolve(body)); req.on('error', (err) => reject(err)); });