Skip to content

Commit 85d119f

Browse files
feat: csrf middleware (#1182)
* create csrf middleware * minor * update csrf token * handle absolute paths * csrf test * replace http to net http * replace AbortWithStatusJson with Abort
1 parent 123d53e commit 85d119f

3 files changed

Lines changed: 396 additions & 0 deletions

File tree

contracts/http/status.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const (
7373
StatusLoopDetected = http.StatusLoopDetected
7474
StatusNotExtended = http.StatusNotExtended
7575
StatusNetworkAuthenticationRequired = http.StatusNetworkAuthenticationRequired
76+
StatusTokenMismatch = 419
7677
)
7778

7879
var statusText = map[int]string{
@@ -142,6 +143,7 @@ var statusText = map[int]string{
142143
StatusLoopDetected: http.StatusText(StatusLoopDetected),
143144
StatusNotExtended: http.StatusText(StatusNotExtended),
144145
StatusNetworkAuthenticationRequired: http.StatusText(StatusNetworkAuthenticationRequired),
146+
StatusTokenMismatch: "CSRF token mismatch",
145147
}
146148

147149
// StatusText returns a text for the HTTP status code. It returns the empty
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package middleware
2+
3+
import (
4+
"crypto/subtle"
5+
"net/url"
6+
"path"
7+
"strings"
8+
9+
contractshttp "github.com/goravel/framework/contracts/http"
10+
)
11+
12+
const HeaderCsrfKey = "X-CSRF-TOKEN"
13+
14+
func VerifyCsrfToken(excepts []string) contractshttp.Middleware {
15+
absolutePaths := parseExceptPaths(excepts)
16+
return func(ctx contractshttp.Context) {
17+
if isReading(ctx.Request().Method()) || inExceptArray(absolutePaths, ctx.Request().Path()) || tokenMatch(ctx) {
18+
ctx.Request().Next()
19+
ctx.Response().Header(HeaderCsrfKey, ctx.Request().Session().Token())
20+
} else {
21+
ctx.Request().Abort(contractshttp.StatusTokenMismatch)
22+
}
23+
}
24+
}
25+
26+
func tokenMatch(ctx contractshttp.Context) bool {
27+
if !ctx.Request().HasSession() {
28+
return false
29+
}
30+
sessionCsrfToken := ctx.Request().Session().Token()
31+
requestCsrfToken := ctx.Request().Header(HeaderCsrfKey)
32+
if requestCsrfToken == "" {
33+
requestCsrfToken = ctx.Request().Input("_token")
34+
}
35+
if requestCsrfToken == "" || subtle.ConstantTimeCompare([]byte(requestCsrfToken), []byte(sessionCsrfToken)) == 0 {
36+
return false
37+
}
38+
return true
39+
}
40+
41+
func inExceptArray(excepts []string, currentPath string) bool {
42+
currentPath = strings.Trim(currentPath, "/")
43+
for _, pattern := range excepts {
44+
if matched, err := path.Match(pattern, currentPath); err == nil && matched {
45+
return true
46+
}
47+
}
48+
return false
49+
}
50+
51+
func isReading(method string) bool {
52+
return method == contractshttp.MethodGet || method == contractshttp.MethodHead || method == contractshttp.MethodOptions
53+
}
54+
55+
func parseExceptPaths(rawExcepts []string) []string {
56+
var paths []string
57+
for _, except := range rawExcepts {
58+
if u, err := url.Parse(except); err == nil && u.Path != "" {
59+
paths = append(paths, strings.Trim(u.Path, "/"))
60+
} else {
61+
paths = append(paths, strings.Trim(except, "/"))
62+
}
63+
}
64+
return paths
65+
}

0 commit comments

Comments
 (0)