Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSRF in NextJS with next-http-proxy-middleware conflict #57

Open
mkoumila opened this issue Aug 3, 2024 · 6 comments
Open

CSRF in NextJS with next-http-proxy-middleware conflict #57

mkoumila opened this issue Aug 3, 2024 · 6 comments

Comments

@mkoumila
Copy link

mkoumila commented Aug 3, 2024

Hello @amorey ,
I am currently facing an issue using @edge-csrf/node-http with next-http-proxy-middleware in my Next.js 14 project. My project is connected to a Drupal CMS backend. In brief, I have a form in my Next.js project that sends a POST request to Drupal.

Everything was working fine when I was using the CSRF protection from next-auth/react. However, after implementing @edge-csrf/node-http in my custom Node.js server within my Next.js project, I receive the following response instead of the actual data (with a 400 error) when sending a POST request:

array:1 [ "{"type":"Buffer","data":_100,111,99,117,109,101,110,116,95,99,118,61,38,99,115,114,102,95,116,111,107,101,110,61,65,65,104,83,116,67,51,8" => "" ]

Is there something I might have missed adding to my proxy code, such as an @edge-csrf functionality or configuration?

All my requests passes by this proxy:

// api/proxy
import httpProxyMiddleware from "next-http-proxy-middleware"
import { redisOffline } from "@custompackage/core/server"
import * as crypto from "crypto"
import { getServerSidePropsFlags } from "@custompackage/console/server"

if (process.env.DRUPAL_BASE_URL === undefined) {
	throw Error("DRUPAL BASE URL environment variable not specified!")
}

const handleProxyInit = (proxy) => {
	/**
	 * Check the list of bindable events in the `http-proxy` specification.
	 * @see https://www.npmjs.com/package/http-proxy#listening-for-proxy-events
	 */
	proxy.on("proxyReq", () => {})
	proxy.on("proxyRes", async (proxyRes, req) => {
		if (req.method !== "GET") {
			return
		}

		if (
			proxyRes.headers["content-type"].includes("json") &&
			proxyRes.statusCode === 200
		) {
			const uniqueReqId = crypto.createHash("sha512").update(req.url).digest("hex")
			const cacheKey = `apiproxy:${uniqueReqId}`

			var body = []
			proxyRes.on("data", function (chunk) {
				body.push(chunk)
			})

			proxyRes.on("end", async function () {
				body = Buffer.concat(body).toString()
				try {
					// make sure to validate the JSON before setting it in the cache
					JSON.parse(body)
					await redisOffline.set(cacheKey, body)
				} catch (e) {
					console.error(`Cannot parse JSON for ${req.url}`, e)
				}
			})
		}
	})
}

export default async function handler(req, res) {
	try {
		if (
			req.method === "GET" &&
			getServerSidePropsFlags().serverFlags.get("enableOffline")
		) {
			const uniqueReqId = crypto
				.createHash("sha512")
				.update(req.url.replace(new RegExp(`^/api/proxy`, "i"), ""))
				.digest("hex")
			const cacheKey = `apiproxy:${uniqueReqId}`

			const cached = await redisOffline.get(cacheKey)

			if (cached) {
				return res.end(cached)
			}
		}

		// API resolved without sending a response for ..., this may result in stalled requests.
		return await httpProxyMiddleware(req, res, {
			onProxyInit: handleProxyInit,
			target: process.env.DRUPAL_BASE_URL,
			secure: false, // Don't verify the SSL Certs
			pathRewrite: [{ patternStr: `^/api/proxy`, replaceStr: "" }],
			followRedirects: true,
			headers: {
				cookie: "", // Must override the browser sent authorization code otherwise ingress gives a 400 status
			},
		})
	} catch (error) {
		console.error(error)
		res.status(500).json(error)
	}
}

export const config = {
	api: {
		bodyParser: false,
		externalResolver: true, // Prevents noise created by proxy
	},
}

Thank you for your assist

@mkoumila
Copy link
Author

mkoumila commented Aug 4, 2024

Here is a request/response data regarding sending a POST request with Proxy ( failed ) and Without it ( successful ):

PS: Some data has been changed to "bla bla bla" or fake tokens for security 🙏🏻

// REQUEST - No Proxy:

POST /fr/_webform HTTP/1.1
Accept: application/vnd.api+json
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Length: 141
Content-Type: application/x-www-form-urlencoded
Host: localhost:8080
Origin: http://localhost:3000
Referer: http://localhost:3000/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: "Bla Bla Bla"
sec-ch-ua: "Bla Bla Bla"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"

// REQUEST - with Proxy:

POST /api/proxy/fr/_webform HTTP/1.1
Accept: application/vnd.api+json
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Length: 137
Content-Type: application/x-www-form-urlencoded
Cookie: _csrfSecret=pYsh6lqK0X54R4Blo1BAA8KP;
Host: localhost:3000
Origin: http://localhost:3000
Referer: http://localhost:3000/contact
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: "Bla Bla Bla"
sec-ch-ua: "Bla Bla Bla"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"

// RESPONSE - No Proxy:

HTTP/1.1 200 OK
Server: nginx/1.blabla.1
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: must-revalidate, no-cache, private
Date: Sat, 03 Aug 2024 23:36:22 GMT
Content-language: fr
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sun, 19 Nov 1978 05:00:00 GMT
X-Consumer-ID: default_consumer
Vary: X-Consumer-ID
Access-Control-Allow-Origin: *
Content-Encoding: gzip

//
RESPONSE - with Proxy:

HTTP/1.1 400 Bad Request
X-CSRF-Token: AAiXzolJChmvYbrK7fAOYQvspCsr8yYU+S4pQ5PA
Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-eval' 'unsafe-inline' style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; frame-src 'self'; connect-src 'self'; media-src 'self'; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';
X-DNS-Prefetch-Control: on
Strict-Transport-Security: "bla bla bla"
X-XSS-Protection: "bla bla bla"
x-frame-options: SAMEORIGIN
Permissions-Policy: camera=(), microphone=(), geolocation=()
x-content-type-options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
server: nginx/1.blabla.1
content-type: application/json
content-length: 75
connection: close
cache-control: must-revalidate, no-cache, private
date: Sat, 03 Aug 2024 23:37:54 GMT
content-language: fr
expires: Sun, 19 Nov 1978 05:00:00 GMT
x-consumer-id: default_consumer
vary: X-Consumer-ID
access-control-allow-origin: *

@amorey
Copy link
Member

amorey commented Aug 5, 2024

return await httpProxyMiddleware(req, res, {
onProxyInit: handleProxyInit,
target: process.env.DRUPAL_BASE_URL,
secure: false, // Don't verify the SSL Certs
pathRewrite: [{ patternStr: ^/api/proxy, replaceStr: "" }],
followRedirects: true,
headers: {
cookie: "", // Must override the browser sent authorization code otherwise ingress gives a 400 status
},
})

It looks like the request to httpProxyMiddleware() isn't forwarding cookies from the client so all requests will fail validation. Can you try forwarding the csrf cookie and see if that fixes the problem?

@mkoumila
Copy link
Author

mkoumila commented Aug 5, 2024

Hi @amorey ,
i remove this line yet i still get the same error:

headers: {
      cookie: ""
},

The problem to be specific is that my form payload on POST request is:

csrf_token: AAgSqrQ/8R2JonL8SIczZmmACaxjNM2R5ebIjdPT
name: myname
webform_id: form_test

and the error is get is that webform_id is null even thou it's present in the payload ! but when i remove CSRF it works just fine.

It might be a conflict between CSRF and Http Proxy i guess since the data i receive in the backoffice is a Buffer!

To fix this issues, Would you please @amorey give us the detailed steps on how CSRF get validated from the creating of it ? This would be super helpful !

Thanks

@amorey
Copy link
Member

amorey commented Aug 5, 2024

Here's the code block that gets the token from the request (getTokenString()) and passes it to the verification function (verifyToken()):
https://github.com/kubetail-org/edge-csrf/blob/main/shared/src/protect.ts#L136-L142

Can you share a minimal example that demonstrates the error?

@mkoumila
Copy link
Author

mkoumila commented Aug 5, 2024

Thanks @amorey,

I wish i could, but it's a company project ( for a client ) and it's huge, making a minimal example would take a lot because it has multiple functionalities and packages 🙏🏻
Somehow i understand what is the problem i'll debug more to fix it.

I appreciate your help buddy 😄

@mkoumila
Copy link
Author

FIX: #59

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants