Skip to content

Commit 1c63ce4

Browse files
authored
vweb: adding a vweb.csrf protection module (#15586)
1 parent 95a328b commit 1c63ce4

File tree

5 files changed

+155
-0
lines changed

5 files changed

+155
-0
lines changed

cmd/tools/vtest-self.v

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const (
127127
'vlib/v/tests/orm_joined_tables_select_test.v',
128128
'vlib/v/tests/sql_statement_inside_fn_call_test.v',
129129
'vlib/vweb/tests/vweb_test.v',
130+
'vlib/vweb/csrf/csrf_test.v',
130131
'vlib/vweb/request_test.v',
131132
'vlib/net/http/request_test.v',
132133
'vlib/net/http/response_test.v',
@@ -172,6 +173,7 @@ const (
172173
'vlib/clipboard/clipboard_test.v',
173174
'vlib/vweb/tests/vweb_test.v',
174175
'vlib/vweb/request_test.v',
176+
'vlib/vweb/csrf/csrf_test.v',
175177
'vlib/net/http/request_test.v',
176178
'vlib/vweb/route_test.v',
177179
'vlib/net/websocket/websocket_test.v',

vlib/vweb/csrf/create_cookie.v

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
module csrf
2+
3+
import rand
4+
5+
const chars = 'QWERTZUIOPASDFGHJKLYXCVBNMqwertzuiopasdfghjklyxcvbnm1234567890_-'
6+
7+
const cookie_key = '__Host-Csrf-Token'
8+
9+
// set_csrf_cookie - generates a CSRF-Token and sets the CSRF-Cookie. It is possible to set the http-only-status of the cookie to false by adding an argument of the HttpOnly-struct like this:
10+
// `app.set_csrf_cookie(csrf.HttpOnly{false})`
11+
// If no argument is set, http_only will be set to `true`by default.
12+
pub fn (mut app App) set_csrf_cookie(h ...HttpOnly) App {
13+
mut http_only := true
14+
if h.len > 0 {
15+
http_only = h[0].http_only
16+
}
17+
cookie := create_cookie(http_only)
18+
app = App{app.Context, cookie.value}
19+
app.set_cookie(cookie)
20+
return app
21+
}
22+
23+
// generate - generates the CSRF-Token
24+
fn generate() string {
25+
mut out := ''
26+
for _ in 0 .. 42 {
27+
i := rand.intn(csrf.chars.len_utf8()) or {
28+
panic('Error while trying to generate Csrf-Token: $err')
29+
}
30+
out = out + csrf.chars[i..i + 1]
31+
}
32+
return out
33+
}
34+
35+
// create_cookie - creates the cookie
36+
fn create_cookie(h bool) CsrfCookie {
37+
return CsrfCookie{
38+
name: csrf.cookie_key
39+
value: generate()
40+
path: '/'
41+
max_age: 0
42+
secure: true
43+
http_only: h
44+
}
45+
}
46+
47+
// get_csrf_token - returns the CSRF-Token that has been set. Make sure that you set one by using `set_csrf_cookie()`. If it's value is empty or no cookie has been generated, the function will thor an error.
48+
pub fn (mut app App) get_csrf_token() ?string {
49+
if app.csrf_cookie_value != '' {
50+
return app.csrf_cookie_value
51+
} else {
52+
return IError(CsrfError{
53+
m: 'The CSRF-Token-Value is empty. Please check if you have setted a cookie!'
54+
})
55+
}
56+
}

vlib/vweb/csrf/csrf_test.v

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import time
2+
import net.http
3+
import vweb
4+
import vweb.csrf
5+
6+
const sport = 10801
7+
8+
struct App {
9+
csrf.App
10+
}
11+
12+
// index - will handle requests to path '/'
13+
fn (mut app App) index() vweb.Result {
14+
// Set a Csrf-Cookie(Token will be generated automatically) and set http_only-status. If no argument ist passed, it will be true by default.
15+
app.set_csrf_cookie(csrf.HttpOnly{false})
16+
// Get the token-value from the csrf-cookie that was just setted
17+
token := app.get_csrf_token() or { panic(err) }
18+
return app.text("Csrf-Token set! It's value is: $token")
19+
}
20+
21+
fn test_send_a_request_to_homepage_expecting_a_csrf_cookie() ? {
22+
go vweb.run_at(&App{}, vweb.RunParams{ port: sport })
23+
time.sleep(500 * time.millisecond)
24+
res := http.get('http://localhost:$sport/')?
25+
if res.header.str().contains('__Host-Csrf-Token') {
26+
assert true
27+
} else {
28+
assert false
29+
}
30+
}

vlib/vweb/csrf/protect.v

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
module csrf
2+
3+
import net.http
4+
5+
// csrf_protect - protects a handler-function against CSRF. Should be set at the beginning of the handler-function.
6+
pub fn (mut app App) csrf_protect() CheckedApp {
7+
req_cookies := app.req.cookies.clone()
8+
app_csrf_cookie_str := app.get_cookie(cookie_key) or {
9+
// Do not return normally!! No Csrf-Token was set!
10+
app.set_status(403, '')
11+
return app.text('Error 403 - Forbidden')
12+
}
13+
14+
if cookie_key in req_cookies && req_cookies[cookie_key] == app_csrf_cookie_str {
15+
// Csrf-Check OK - return app as normal in order to handle request normally
16+
return app
17+
} else if app.check_headers(app_csrf_cookie_str) {
18+
// Csrf-Check OK - return app as normal in order to handle request normally
19+
return app
20+
} else {
21+
// Do not return normally!! The client has not passed the Csrf-Check!!
22+
app.set_status(403, '')
23+
return app.text('Error 403 - Forbidden')
24+
}
25+
}
26+
27+
// check_headers - checks if there is a CSRF-Token that was sent with the headers of a request
28+
fn (app App) check_headers(app_csrf_cookie_str string) bool {
29+
token := app.req.header.get_custom('Csrf-Token', http.HeaderQueryConfig{true}) or {
30+
return false
31+
}
32+
if token == app_csrf_cookie_str {
33+
return true
34+
} else {
35+
return false
36+
}
37+
}

vlib/vweb/csrf/structs.v

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// This module provides csrf-protection for apps written with libe vweb.
2+
3+
module csrf
4+
5+
import vweb
6+
import net.http
7+
8+
type CsrfCookie = http.Cookie
9+
10+
interface CheckedApp {}
11+
12+
pub struct App {
13+
vweb.Context
14+
csrf_cookie_value string
15+
}
16+
17+
pub struct HttpOnly {
18+
http_only bool
19+
}
20+
21+
struct CsrfError {
22+
Error
23+
m string
24+
}
25+
26+
fn (err CsrfError) msg() string {
27+
return err.m
28+
}
29+
30+
// Written by flopetautschnig (floscodes) 2022

0 commit comments

Comments
 (0)