@@ -13,7 +13,7 @@ describe('csrf handler', () => {
13
13
{
14
14
contentType : 'text/plain' ,
15
15
} ,
16
- ] ) ( 'should throw an error if the origin does not match for $contentType' , ( { contentType } ) => {
16
+ ] ) ( 'should reject request when the origin does not match for $contentType' , ( { contentType } ) => {
17
17
const errorFn = vi . fn ( ) ;
18
18
const requestEv = {
19
19
request : {
@@ -26,13 +26,131 @@ describe('csrf handler', () => {
26
26
error : errorFn ,
27
27
} as unknown as RequestEvent ;
28
28
29
+ expect ( ( ) => csrfCheckMiddleware ( requestEv ) ) . toThrow ( ) ;
30
+ expect ( errorFn ) . toBeCalledWith ( 403 , expect . stringMatching ( 'CSRF check failed' ) ) ;
31
+ } ) ;
32
+
33
+ it ( 'should reject request when origin header is missing for form content types' , ( ) => {
34
+ const errorFn = vi . fn ( ) ;
35
+ const requestEv = {
36
+ request : {
37
+ headers : new Headers ( {
38
+ 'content-type' : 'application/x-www-form-urlencoded' ,
39
+ // No origin header
40
+ } ) ,
41
+ } ,
42
+ url : new URL ( 'http://example.com' ) ,
43
+ error : errorFn ,
44
+ } as unknown as RequestEvent ;
45
+
46
+ expect ( ( ) => csrfCheckMiddleware ( requestEv ) ) . toThrow ( ) ;
47
+ expect ( errorFn ) . toBeCalledWith ( 403 , expect . stringMatching ( 'CSRF check failed' ) ) ;
48
+ } ) ;
49
+
50
+ it . each ( [
51
+ {
52
+ contentType : 'application/x-www-form-urlencoded' ,
53
+ } ,
54
+ {
55
+ contentType : 'multipart/form-data' ,
56
+ } ,
57
+ {
58
+ contentType : 'text/plain' ,
59
+ } ,
60
+ ] ) ( 'should allow request when origin matches for $contentType' , ( { contentType } ) => {
61
+ const errorFn = vi . fn ( ) ;
62
+ const requestEv = {
63
+ request : {
64
+ headers : new Headers ( {
65
+ 'content-type' : contentType ,
66
+ origin : 'http://example.com' ,
67
+ } ) ,
68
+ } ,
69
+ url : new URL ( 'http://example.com' ) ,
70
+ error : errorFn ,
71
+ } as unknown as RequestEvent ;
72
+
73
+ // Should not throw an error
74
+ expect ( ( ) => csrfCheckMiddleware ( requestEv ) ) . not . toThrow ( ) ;
75
+ expect ( errorFn ) . not . toHaveBeenCalled ( ) ;
76
+ } ) ;
77
+
78
+ it . each ( [
79
+ {
80
+ contentType : 'application/json' ,
81
+ } ,
82
+ {
83
+ contentType : 'text/html' ,
84
+ } ,
85
+ {
86
+ contentType : 'application/xml' ,
87
+ } ,
88
+ {
89
+ contentType : 'image/png' ,
90
+ } ,
91
+ ] ) (
92
+ 'should allow request for non-form content type $contentType regardless of origin' ,
93
+ ( { contentType } ) => {
94
+ const errorFn = vi . fn ( ) ;
95
+ const requestEv = {
96
+ request : {
97
+ headers : new Headers ( {
98
+ 'content-type' : contentType ,
99
+ origin : 'http://example.com' ,
100
+ } ) ,
101
+ } ,
102
+ url : new URL ( 'http://bad-example.com' ) ,
103
+ error : errorFn ,
104
+ } as unknown as RequestEvent ;
105
+
106
+ // Should not throw an error for non-form content types
107
+ expect ( ( ) => csrfCheckMiddleware ( requestEv ) ) . not . toThrow ( ) ;
108
+ expect ( errorFn ) . not . toHaveBeenCalled ( ) ;
109
+ }
110
+ ) ;
111
+
112
+ it ( 'should allow request when content-type header is missing' , ( ) => {
113
+ const errorFn = vi . fn ( ) ;
114
+ const requestEv = {
115
+ request : {
116
+ headers : new Headers ( {
117
+ origin : 'http://example.com' ,
118
+ // No content-type header
119
+ } ) ,
120
+ } ,
121
+ url : new URL ( 'http://example.com' ) ,
122
+ error : errorFn ,
123
+ } as unknown as RequestEvent ;
124
+
125
+ // Should not throw an error when content-type is missing
126
+ expect ( ( ) => csrfCheckMiddleware ( requestEv ) ) . not . toThrow ( ) ;
127
+ expect ( errorFn ) . not . toHaveBeenCalled ( ) ;
128
+ } ) ;
129
+
130
+ it ( 'should verify exact error message content' , ( ) => {
131
+ const errorFn = vi . fn ( ) ;
132
+ const requestEv = {
133
+ request : {
134
+ headers : new Headers ( {
135
+ 'content-type' : 'application/x-www-form-urlencoded' ,
136
+ origin : 'http://malicious.com' ,
137
+ } ) ,
138
+ } ,
139
+ url : new URL ( 'http://example.com' ) ,
140
+ method : 'POST' ,
141
+ error : errorFn ,
142
+ } as unknown as RequestEvent ;
143
+
29
144
try {
30
145
csrfCheckMiddleware ( requestEv ) ;
31
146
} catch ( _ ) {
32
147
// ignore the error here, we just want to check the errorFn
33
148
}
34
149
35
- expect ( errorFn ) . toBeCalledWith ( 403 , expect . stringMatching ( 'CSRF check failed' ) ) ;
150
+ expect ( errorFn ) . toBeCalledWith (
151
+ 403 ,
152
+ 'CSRF check failed. Cross-site POST form submissions are forbidden.\nThe request origin "http://malicious.com" does not match the server origin "http://example.com".'
153
+ ) ;
36
154
} ) ;
37
155
38
156
describe ( 'isContentType' , ( ) => {
@@ -43,5 +161,53 @@ describe('csrf handler', () => {
43
161
} ) ;
44
162
expect ( isContentType ( headers , 'multipart/form-data' ) ) . toBe ( true ) ;
45
163
} ) ;
164
+
165
+ it ( 'should handle multiple content type parameters' , ( ) => {
166
+ const headers = new Headers ( {
167
+ 'content-type' : 'application/x-www-form-urlencoded; charset=utf-8' ,
168
+ } ) ;
169
+ expect ( isContentType ( headers , 'application/x-www-form-urlencoded' ) ) . toBe ( true ) ;
170
+ } ) ;
171
+
172
+ it ( 'should handle case insensitive content types' , ( ) => {
173
+ const headers = new Headers ( {
174
+ 'content-type' : 'APPLICATION/X-WWW-FORM-URLENCODED' ,
175
+ } ) ;
176
+ expect ( isContentType ( headers , 'application/x-www-form-urlencoded' ) ) . toBe ( false ) ;
177
+ } ) ;
178
+
179
+ it ( 'should return false for non-matching content types' , ( ) => {
180
+ const headers = new Headers ( {
181
+ 'content-type' : 'application/json' ,
182
+ } ) ;
183
+ expect ( isContentType ( headers , 'application/x-www-form-urlencoded' ) ) . toBe ( false ) ;
184
+ } ) ;
185
+
186
+ it ( 'should handle empty content-type header' , ( ) => {
187
+ const headers = new Headers ( { } ) ;
188
+ expect ( isContentType ( headers , 'application/x-www-form-urlencoded' ) ) . toBe ( false ) ;
189
+ } ) ;
190
+
191
+ it ( 'should handle missing content-type header' , ( ) => {
192
+ const headers = new Headers ( {
193
+ 'other-header' : 'value' ,
194
+ } ) ;
195
+ expect ( isContentType ( headers , 'application/x-www-form-urlencoded' ) ) . toBe ( false ) ;
196
+ } ) ;
197
+
198
+ it ( 'should handle multiple content type checks' , ( ) => {
199
+ const headers = new Headers ( {
200
+ 'content-type' : 'text/plain' ,
201
+ } ) ;
202
+ expect ( isContentType ( headers , 'application/x-www-form-urlencoded' , 'text/plain' ) ) . toBe ( true ) ;
203
+ expect ( isContentType ( headers , 'application/json' , 'multipart/form-data' ) ) . toBe ( false ) ;
204
+ } ) ;
205
+
206
+ it ( 'should handle content type with only whitespace' , ( ) => {
207
+ const headers = new Headers ( {
208
+ 'content-type' : ' ' ,
209
+ } ) ;
210
+ expect ( isContentType ( headers , 'application/x-www-form-urlencoded' ) ) . toBe ( false ) ;
211
+ } ) ;
46
212
} ) ;
47
213
} ) ;
0 commit comments