@@ -13,7 +13,7 @@ describe('csrf handler', () => {
1313 {
1414 contentType : 'text/plain' ,
1515 } ,
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 } ) => {
1717 const errorFn = vi . fn ( ) ;
1818 const requestEv = {
1919 request : {
@@ -26,13 +26,131 @@ describe('csrf handler', () => {
2626 error : errorFn ,
2727 } as unknown as RequestEvent ;
2828
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+
29144 try {
30145 csrfCheckMiddleware ( requestEv ) ;
31146 } catch ( _ ) {
32147 // ignore the error here, we just want to check the errorFn
33148 }
34149
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+ ) ;
36154 } ) ;
37155
38156 describe ( 'isContentType' , ( ) => {
@@ -43,5 +161,53 @@ describe('csrf handler', () => {
43161 } ) ;
44162 expect ( isContentType ( headers , 'multipart/form-data' ) ) . toBe ( true ) ;
45163 } ) ;
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+ } ) ;
46212 } ) ;
47213} ) ;
0 commit comments