diff --git a/cmd/tools/vtest-self.v b/cmd/tools/vtest-self.v index 2224cd3342..3bcbe95228 100644 --- a/cmd/tools/vtest-self.v +++ b/cmd/tools/vtest-self.v @@ -127,6 +127,7 @@ const ( 'vlib/v/tests/orm_joined_tables_select_test.v', 'vlib/v/tests/sql_statement_inside_fn_call_test.v', 'vlib/vweb/tests/vweb_test.v', + 'vlib/vweb/csrf/csrf_test.v', 'vlib/vweb/request_test.v', 'vlib/net/http/request_test.v', 'vlib/net/http/response_test.v', @@ -172,6 +173,7 @@ const ( 'vlib/clipboard/clipboard_test.v', 'vlib/vweb/tests/vweb_test.v', 'vlib/vweb/request_test.v', + 'vlib/vweb/csrf/csrf_test.v', 'vlib/net/http/request_test.v', 'vlib/vweb/route_test.v', 'vlib/net/websocket/websocket_test.v', diff --git a/vlib/vweb/csrf/create_cookie.v b/vlib/vweb/csrf/create_cookie.v new file mode 100644 index 0000000000..e23e9fed06 --- /dev/null +++ b/vlib/vweb/csrf/create_cookie.v @@ -0,0 +1,56 @@ +module csrf + +import rand + +const chars = 'QWERTZUIOPASDFGHJKLYXCVBNMqwertzuiopasdfghjklyxcvbnm1234567890_-' + +const cookie_key = '__Host-Csrf-Token' + +// 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: +// `app.set_csrf_cookie(csrf.HttpOnly{false})` +// If no argument is set, http_only will be set to `true`by default. +pub fn (mut app App) set_csrf_cookie(h ...HttpOnly) App { + mut http_only := true + if h.len > 0 { + http_only = h[0].http_only + } + cookie := create_cookie(http_only) + app = App{app.Context, cookie.value} + app.set_cookie(cookie) + return app +} + +// generate - generates the CSRF-Token +fn generate() string { + mut out := '' + for _ in 0 .. 42 { + i := rand.intn(csrf.chars.len_utf8()) or { + panic('Error while trying to generate Csrf-Token: $err') + } + out = out + csrf.chars[i..i + 1] + } + return out +} + +// create_cookie - creates the cookie +fn create_cookie(h bool) CsrfCookie { + return CsrfCookie{ + name: csrf.cookie_key + value: generate() + path: '/' + max_age: 0 + secure: true + http_only: h + } +} + +// 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. +pub fn (mut app App) get_csrf_token() ?string { + if app.csrf_cookie_value != '' { + return app.csrf_cookie_value + } else { + return IError(CsrfError{ + m: 'The CSRF-Token-Value is empty. Please check if you have setted a cookie!' + }) + } +} diff --git a/vlib/vweb/csrf/csrf_test.v b/vlib/vweb/csrf/csrf_test.v new file mode 100644 index 0000000000..f364fa76d8 --- /dev/null +++ b/vlib/vweb/csrf/csrf_test.v @@ -0,0 +1,30 @@ +import time +import net.http +import vweb +import vweb.csrf + +const sport = 10801 + +struct App { + csrf.App +} + +// index - will handle requests to path '/' +fn (mut app App) index() vweb.Result { + // 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. + app.set_csrf_cookie(csrf.HttpOnly{false}) + // Get the token-value from the csrf-cookie that was just setted + token := app.get_csrf_token() or { panic(err) } + return app.text("Csrf-Token set! It's value is: $token") +} + +fn test_send_a_request_to_homepage_expecting_a_csrf_cookie() ? { + go vweb.run_at(&App{}, vweb.RunParams{ port: sport }) + time.sleep(500 * time.millisecond) + res := http.get('http://localhost:$sport/')? + if res.header.str().contains('__Host-Csrf-Token') { + assert true + } else { + assert false + } +} diff --git a/vlib/vweb/csrf/protect.v b/vlib/vweb/csrf/protect.v new file mode 100644 index 0000000000..2b460d51e6 --- /dev/null +++ b/vlib/vweb/csrf/protect.v @@ -0,0 +1,37 @@ +module csrf + +import net.http + +// csrf_protect - protects a handler-function against CSRF. Should be set at the beginning of the handler-function. +pub fn (mut app App) csrf_protect() CheckedApp { + req_cookies := app.req.cookies.clone() + app_csrf_cookie_str := app.get_cookie(cookie_key) or { + // Do not return normally!! No Csrf-Token was set! + app.set_status(403, '') + return app.text('Error 403 - Forbidden') + } + + if cookie_key in req_cookies && req_cookies[cookie_key] == app_csrf_cookie_str { + // Csrf-Check OK - return app as normal in order to handle request normally + return app + } else if app.check_headers(app_csrf_cookie_str) { + // Csrf-Check OK - return app as normal in order to handle request normally + return app + } else { + // Do not return normally!! The client has not passed the Csrf-Check!! + app.set_status(403, '') + return app.text('Error 403 - Forbidden') + } +} + +// check_headers - checks if there is a CSRF-Token that was sent with the headers of a request +fn (app App) check_headers(app_csrf_cookie_str string) bool { + token := app.req.header.get_custom('Csrf-Token', http.HeaderQueryConfig{true}) or { + return false + } + if token == app_csrf_cookie_str { + return true + } else { + return false + } +} diff --git a/vlib/vweb/csrf/structs.v b/vlib/vweb/csrf/structs.v new file mode 100644 index 0000000000..09b648b37a --- /dev/null +++ b/vlib/vweb/csrf/structs.v @@ -0,0 +1,30 @@ +// This module provides csrf-protection for apps written with libe vweb. + +module csrf + +import vweb +import net.http + +type CsrfCookie = http.Cookie + +interface CheckedApp {} + +pub struct App { + vweb.Context + csrf_cookie_value string +} + +pub struct HttpOnly { + http_only bool +} + +struct CsrfError { + Error + m string +} + +fn (err CsrfError) msg() string { + return err.m +} + +// Written by flopetautschnig (floscodes) 2022