Skip to content

feat: csrf middleware#1182

Merged
hwbrzzl merged 7 commits intogoravel:masterfrom
NiteshSingh17:NiteshSingh17/create-csrf-middleware
Sep 12, 2025
Merged

feat: csrf middleware#1182
hwbrzzl merged 7 commits intogoravel:masterfrom
NiteshSingh17:NiteshSingh17/create-csrf-middleware

Conversation

@NiteshSingh17
Copy link
Contributor

📑 Description

Closes goravel/goravel#218

✅ Checks

  • Added test cases for my code

@NiteshSingh17 NiteshSingh17 requested a review from a team as a code owner September 4, 2025 19:22
@codecov
Copy link

codecov bot commented Sep 4, 2025

Codecov Report

❌ Patch coverage is 85.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.90%. Comparing base (f9cbddb) to head (6aca39b).
⚠️ Report is 11 commits behind head on master.

Files with missing lines Patch % Lines
http/middleware/verify_csrf_token.go 85.00% 4 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1182      +/-   ##
==========================================
+ Coverage   68.86%   68.90%   +0.04%     
==========================================
  Files         230      236       +6     
  Lines       14879    15426     +547     
==========================================
+ Hits        10246    10630     +384     
- Misses       4235     4410     +175     
+ Partials      398      386      -12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@NiteshSingh17 NiteshSingh17 changed the title Nitesh singh17/create csrf middleware feat: csrf middleware Sep 4, 2025
Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, great PR. Could you implement the except logic as well?

image


func CSRFToken() httpcontract.Middleware {
return func(ctx httpcontract.Context) {
sessionCSRFTokenInterface := ctx.Request().Session().Get(csrfKey)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session csrf token can be obtained using the Token method. Make sure the session is set, otherwise it will panic. You can use the HasSession method from Request() to check this.

Copy link
Contributor Author

@NiteshSingh17 NiteshSingh17 Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kkumar-gcc,

I tested the regenerate function, which is responsible for generating a new CSRF token and setting it on _token. However, I noticed that when I send a subsequent request and access the token using Session.Token(), it still returns the token from the previous request not the updated one generated in the current request.

Could you please take a look and test this behavior? This could potentially lead to issues in the future when passing the _token to HTML forms, as the form would use an outdated CSRF token.

Here’s the middleware function I'm currently using for testing:


func VerifyCsrfToken(excepts []string) contractshttp.Middleware {
	return func(ctx contractshttp.Context) {
		if isReading(ctx.Request().Method()) || inExceptArray(excepts, ctx.Request().FullUrl()) || tokenMatch(ctx) {
			ctx.Request().Next()
			ctx.Request().Session().Regenerate()
			ctx.Request().Session().Put(csrfKey, ctx.Request().Session().Token())
		} else {
			ctx.Request().AbortWithStatusJson(contractshttp.StatusTokenMismatch, map[string]string{
				"message": "CSRF token mismatch.",
			})
		}
	}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that’s expected behavior. The CSRF token in session isn’t meant to change on every request. It only needs to be rotated on important events like login, logout, privilege changes, or when the session is migrated. Rotating it per request would cause unnecessary complexity without adding real security benefits. Though we can expose session.RegenerateToken() in case we ever need to manually force a new token in any flow.

cc: @hwbrzzl

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the regenerate function, which is responsible for generating a new CSRF token and setting it on _token. However, I noticed that when I send a subsequent request and access the token using Session.Token(), it still returns the token from the previous request not the updated one generated in the current request.

@NiteshSingh17 Could you add some screenshots for your test? I'm not sure if I got the correct information on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screencast.from.2025-09-07.21-32-35.mp4

I thought there might be an issue because I was expecting the CSRF token to reset with every request. But keeping it the same throughout the session makes sense and works fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to Save the session after regenerating.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save will be automatically called in the StartSession middleware. In Laravel, they also regenerate the token only on specific events instead of on every request. According to the current implementation, VerifyCsrfValidation will only work with the StartSession middleware (which is expected).

Screenshot 2025-09-08 at 3 06 23 PM

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, LGTM, could you add some screenshots to confirm if the middleware is worked as expected?

@NiteshSingh17
Copy link
Contributor Author

Schrrenshots

Middelware setup
Screenshot from 2025-09-07 21-20-50

Protecetd page withou CSRF
Screenshot from 2025-09-07 21-22-06

Unprojtected page without CSRF
Screenshot from 2025-09-07 21-21-24

Protected page with correct CSRF
Screenshot from 2025-09-07 21-22-43
Screenshot from 2025-09-07 21-23-54

ctx.Request().Next()
ctx.Response().Header(HeaderCsrfKey, ctx.Request().Session().Token())
} else {
ctx.Request().AbortWithStatusJson(contractshttp.StatusTokenMismatch, map[string]string{"message": contractshttp.StatusText(contractshttp.StatusTokenMismatch)})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbortWithStatusJson is deprecated, please use ctx.Response().Json().Abort() instead.

if requestCsrfToken == "" {
requestCsrfToken = ctx.Request().Input("_token")
}
if requestCsrfToken == "" || subtle.ConstantTimeCompare([]byte(requestCsrfToken), []byte(sessionCsrfToken)) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They may be compared by length first, otherwise, ConstantTimeCompare will panic.

Suggested change
if requestCsrfToken == "" || subtle.ConstantTimeCompare([]byte(requestCsrfToken), []byte(sessionCsrfToken)) == 0 {
if requestCsrfToken == "" || len(requestCsrfToken) !=len(sessionCsrfToken) || subtle.ConstantTimeCompare([]byte(requestCsrfToken), []byte(sessionCsrfToken)) == 0 {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot from 2025-09-11 20-31-48

subtle.ConstantTimeCompare performs the comparison internally and does not panic in the event of a length mismatch."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the test cases! It's a bit complex. Actually, contractshttp.Context can be mocked, and we can just test the functions in verify_csrf_token.go one by one. But it doesn't matter, you have done great work, we can merge this PR first if you want. You or I can optimize the test cases later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the implementation was a bit complex. Thank you for your support. We can go ahead and merge the PR now, and optimize the test cases later 👍

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome 👍

@hwbrzzl
Copy link
Contributor

hwbrzzl commented Sep 12, 2025

@NiteshSingh17 We can optimize the gin PR and create a PR for fiber next.

@hwbrzzl hwbrzzl merged commit 85d119f into goravel:master Sep 12, 2025
14 checks passed
alfanzain pushed a commit to alfanzain/goravel-framework that referenced this pull request Sep 15, 2025
* create csrf middleware

* minor

* update csrf token

* handle absolute paths

* csrf test

* replace http to net http

* replace AbortWithStatusJson with Abort
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

Successfully merging this pull request may close these issues.

✨ [Feature] Add CSRF token middleware like Laravel

3 participants