v2 / vlib / veb / csrf / csrf_test.v
358 lines · 291 sloc · 8.59 KB · 344b9afcfe67902dd9660bd3b077f18464d1d114
Raw
1// vtest build: !windows // fasthttp.Server.run is not implemented on windows yet
2import time
3import net.http
4import net.html
5import os
6import veb
7import veb.csrf
8
9const sport = 12385
10const localserver = '127.0.0.1:${sport}'
11const exit_after_time = 12000 // milliseconds
12
13const session_id_cookie_name = 'session_id'
14const csrf_config = &csrf.CsrfConfig{
15 secret: 'my-256bit-secret'
16 allowed_hosts: ['*']
17 session_cookie: session_id_cookie_name
18}
19
20const allowed_origin = 'example.com'
21const csrf_config_origin = csrf.CsrfConfig{
22 secret: 'my-256bit-secret'
23 allowed_hosts: [allowed_origin]
24 session_cookie: session_id_cookie_name
25}
26
27// Test CSRF functions
28// =====================================
29
30fn test_set_token() {
31 mut ctx := veb.Context{}
32
33 token := csrf.set_token(mut ctx, csrf_config)
34
35 cookie := ctx.res.header.get(.set_cookie) or { '' }
36 assert cookie.len != 0
37 assert cookie.starts_with('${csrf_config.cookie_name}=')
38}
39
40fn test_protect() {
41 mut ctx := veb.Context{}
42
43 token := csrf.set_token(mut ctx, csrf_config)
44
45 mut cookie := ctx.res.header.get(.set_cookie) or { '' }
46 // get cookie value from "name=value;"
47 cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
48
49 ctx = veb.Context{
50 form: {
51 csrf_config.token_name: token
52 }
53 req: http.Request{
54 method: .post
55 }
56 }
57 ctx.req.add_cookie(name: csrf_config.cookie_name, value: cookie)
58 valid := csrf.protect(mut ctx, csrf_config)
59
60 assert valid == true
61}
62
63fn test_timeout() {
64 timeout := 1
65 short_time_config := &csrf.CsrfConfig{
66 secret: 'my-256bit-secret'
67 allowed_hosts: ['*']
68 session_cookie: session_id_cookie_name
69 max_age: timeout
70 }
71
72 mut ctx := veb.Context{}
73
74 token := csrf.set_token(mut ctx, short_time_config)
75
76 // after 2 seconds the cookie should expire (maxage)
77 time.sleep(2 * time.second)
78 mut cookie := ctx.res.header.get(.set_cookie) or { '' }
79 // get cookie value from "name=value;"
80 cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
81
82 ctx = veb.Context{
83 form: {
84 short_time_config.token_name: token
85 }
86 req: http.Request{
87 method: .post
88 }
89 }
90 ctx.req.add_cookie(name: short_time_config.cookie_name, value: cookie)
91
92 valid := csrf.protect(mut ctx, short_time_config)
93
94 assert valid == false
95}
96
97fn test_valid_origin() {
98 // valid because both Origin and Referer headers are present
99 token, cookie := get_token_cookie('')
100
101 mut req := http.Request{
102 method: .post
103 }
104 req.add_cookie(name: csrf_config.cookie_name, value: cookie)
105 req.add_header(.origin, 'http://${allowed_origin}')
106 req.add_header(.referer, 'http://${allowed_origin}/test')
107 mut ctx := veb.Context{
108 form: {
109 csrf_config.token_name: token
110 }
111 req: req
112 }
113
114 mut valid := csrf.protect(mut ctx, csrf_config_origin)
115 assert valid == true
116}
117
118fn test_invalid_origin() {
119 // invalid because either the Origin, Referer or neither are present
120 token, cookie := get_token_cookie('')
121
122 mut req := http.Request{
123 method: .post
124 }
125 req.add_cookie(name: csrf_config.cookie_name, value: cookie)
126 req.add_header(.origin, 'http://${allowed_origin}')
127 mut ctx := veb.Context{
128 form: {
129 csrf_config.token_name: token
130 }
131 req: req
132 }
133
134 mut valid := csrf.protect(mut ctx, csrf_config_origin)
135 assert valid == false
136
137 req = http.Request{
138 method: .post
139 }
140 req.add_cookie(name: csrf_config.cookie_name, value: cookie)
141 req.add_header(.referer, 'http://${allowed_origin}/test')
142 ctx = veb.Context{
143 form: {
144 csrf_config.token_name: token
145 }
146 req: req
147 }
148
149 valid = csrf.protect(mut ctx, csrf_config_origin)
150 assert valid == false
151
152 req = http.Request{
153 method: .post
154 }
155 req.add_cookie(name: csrf_config.cookie_name, value: cookie)
156 ctx = veb.Context{
157 form: {
158 csrf_config.token_name: token
159 }
160 req: req
161 }
162
163 valid = csrf.protect(mut ctx, csrf_config_origin)
164 assert valid == false
165}
166
167// Testing App
168// ================================
169
170pub struct Context {
171 veb.Context
172 csrf.CsrfContext
173}
174
175pub struct App {
176 veb.Middleware[Context]
177mut:
178 started chan bool
179}
180
181pub fn (mut app App) before_accept_loop() {
182 app.started <- true
183}
184
185fn (app &App) index(mut ctx Context) veb.Result {
186 ctx.config = *csrf_config
187 ctx.set_csrf_token(mut ctx)
188
189 return ctx.html('<form action="/auth" method="post">
190 ${ctx.csrf_token_input()}
191 <label for="password">Your password:</label>
192 <input type="text" id="password" name="password" placeholder="Your password" />
193</form>')
194}
195
196@[post]
197fn (app &App) auth(mut ctx Context) veb.Result {
198 return ctx.ok('authenticated')
199}
200
201// App cleanup function
202// ======================================
203
204pub fn (mut app App) shutdown(mut ctx Context) veb.Result {
205 spawn app.exit_gracefully()
206 return ctx.ok('good bye')
207}
208
209fn (app &App) exit_gracefully() {
210 eprintln('>> webserver: exit_gracefully')
211 time.sleep(100 * time.millisecond)
212 exit(0)
213}
214
215fn exit_after_timeout(mut app App, timeout_in_ms int) {
216 time.sleep(timeout_in_ms * time.millisecond)
217 eprintln('>> webserver: pid: ${os.getpid()}, exiting ...')
218 app.exit_gracefully()
219
220 eprintln('App timed out!')
221 assert true == false
222}
223
224// Tests for the App
225// ======================================
226
227fn test_run_app_in_background() {
228 mut app := &App{}
229 app.route_use('/auth', csrf.middleware[Context](csrf_config))
230
231 spawn exit_after_timeout(mut app, exit_after_time)
232 spawn veb.run_at[App, Context](mut app, port: sport, family: .ip)
233 _ := <-app.started
234}
235
236fn test_token_input() {
237 res := http.get('http://${localserver}/') or { panic(err) }
238
239 mut doc := html.parse(res.body)
240 inputs := doc.get_tags_by_attribute_value('type', 'hidden')
241 assert inputs.len == 1
242 assert csrf_config.token_name == inputs[0].attributes['name']
243}
244
245fn test_token_roundtrip_from_index_form() {
246 res := http.get('http://${localserver}/') or { panic(err) }
247 assert res.status() == .ok
248
249 cookies := res.cookies()
250 assert cookies.len == 1
251
252 mut doc := html.parse(res.body)
253 inputs := doc.get_tags_by_attribute_value('type', 'hidden')
254 assert inputs.len == 1
255
256 token := inputs[0].attributes['value']
257 assert token.len != 0
258
259 mut req := http.Request{
260 method: .post
261 url: 'http://${localserver}/auth'
262 data: http.url_encode_form_data({
263 csrf_config.token_name: token
264 })
265 }
266 for cookie in cookies {
267 req.add_cookie(cookie)
268 }
269
270 post_res := req.do() or { panic(err) }
271 assert post_res.status() == .ok
272 assert post_res.body == 'authenticated'
273}
274
275// utility function to check whether the route at `path` is protected against csrf
276fn protect_route_util(path string) {
277 mut req := http.Request{
278 method: .post
279 url: 'http://${localserver}/${path}'
280 }
281 mut res := req.do() or { panic(err) }
282 assert res.status() == .forbidden
283
284 // A valid request with CSRF protection should have a cookie session id,
285 // csrftoken in `app.form` and the hmac of that token in a cookie
286 session_id := 'user_session_id'
287 token, cookie := get_token_cookie(session_id)
288
289 header := http.new_header_from_map({
290 http.CommonHeader.origin: 'http://${allowed_origin}'
291 http.CommonHeader.referer: 'http://${allowed_origin}/route'
292 })
293
294 formdata := http.url_encode_form_data({
295 csrf_config.token_name: token
296 })
297
298 // session id is altered: test if session hijacking is possible
299 // if the session id the csrftoken changes so the cookie can't be validated
300 mut cookies := {
301 csrf_config.cookie_name: cookie
302 session_id_cookie_name: 'altered'
303 }
304
305 req = http.Request{
306 method: .post
307 url: 'http://${localserver}/${path}'
308 data: formdata
309 header: header
310 }
311 req.add_cookie(name: csrf_config.cookie_name, value: cookie)
312 req.add_cookie(name: session_id_cookie_name, value: 'altered')
313
314 res = req.do() or { panic(err) }
315 assert res.status() == .forbidden
316
317 req = http.Request{
318 method: .post
319 url: 'http://${localserver}/${path}'
320 data: formdata
321 header: header
322 }
323 req.add_cookie(name: csrf_config.cookie_name, value: cookie)
324 req.add_cookie(name: session_id_cookie_name, value: session_id)
325
326 // Everything is valid now and the request should succeed, since session_id_cookie_name will be session_id
327 res = req.do() or { panic(err) }
328 assert res.status() == .ok
329}
330
331fn test_protect_app() {
332 protect_route_util('/auth')
333}
334
335fn testsuite_end() {
336 // This test is guaranteed to be called last.
337 // It sends a request to the server to shutdown.
338 x := http.get('http://${localserver}/shutdown') or {
339 assert err.msg() == ''
340 return
341 }
342 assert x.status() == .ok
343 assert x.body == 'good bye'
344}
345
346// Utility functions
347
348fn get_token_cookie(session_id string) (string, string) {
349 mut ctx := veb.Context{}
350 ctx.req.add_cookie(name: session_id_cookie_name, value: session_id)
351
352 token := csrf.set_token(mut ctx, csrf_config_origin)
353
354 mut cookie := ctx.res.header.get(.set_cookie) or { '' }
355 // get cookie value from "name=value;"
356 cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
357 return token, cookie
358}
359