1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

vweb: vweb.csrf re-implementation (#18220)

This commit is contained in:
Casper Kuethe 2023-05-21 00:56:17 +02:00 committed by GitHub
parent adcf47dcce
commit d0214a254e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 808 additions and 204 deletions

View File

@ -1039,68 +1039,6 @@ pub fn (mut app App) error() vweb.Result {
}
```
# Cross-Site Request Forgery (CSRF) protection
## Provides protection against Cross-Site Request Forgery
## Usage
When building a csrf-protected service, first of all create a `struct`that implements `csrf.App`
```v ignore
module main
import vweb
import vweb.csrf
// embeds the csrf.App struct in order to empower the struct to protect against CSRF
struct App {
csrf.App
}
```
Start a server e.g. in the main function.
```v ignore
fn main() {
vweb.run_at(&App{}, vweb.RunParams{
port: 8080
}) or { panic(err) }
}
```
### Enable CSRF-protection
Then add a handler-function to define on which route or on which site the CSRF-Token shall be set.
```v ignore
fn (mut app App) index() vweb.Result {
// Set a Csrf-Cookie (Token will be generated automatically)
app.set_csrf_cookie()
// Get the token-value from the csrf-cookie that was just set
token := app.get_csrf_token() or { panic(err) }
return app.text("Csrf-Token set! It's value is: $token")
}
```
If you want to set the cookies's HttpOnly-status to false in order to make it
accessible to scripts on your site, you can do it like this:
`app.set_csrf_cookie(csrf.HttpOnly{false})`
If no argument is passed the value will be set to true by default.
### Protect against CSRF
If you want to protect a route or a site against CSRF just add
`app.csrf_protect()` at the beginning of the handler-function.
```v ignore
fn (mut app App) foo() vweb.Result {
// Protect this handler-function against CSRF
app.csrf_protect()
return app.text("Checked and passed csrf-guard")
}
```
Vweb has built-in csrf protection. Go to the [csrf module](csrf/) to learn how
you can protect your app against CSRF.

237
vlib/vweb/csrf/README.md Normal file
View File

@ -0,0 +1,237 @@
# Cross-Site Request Forgery (CSRF) protection
This module implements the [double submit cookie][owasp] technique to protect routes
from CSRF attacks.
CSRF is a type of attack that occurs when a malicious program/website (and others) causes
a user's web browser to perform an action without them knowing. A web browser automatically sends
cookies to a website when it performs a request, including session cookies. So if a user is
authenticated on your website the website can not distinguish a forged request by a legitimate
request.
## When to not add CSRF-protection
If you are creating a service that is intended to be used by other servers e.g. an API,
you probably don't want CSRF-protection. An alternative would be to send an Authorization
token in, and only in, an HTTP-header (like JSON Web Tokens). If you do that your website
isn't vulnerable to CSRF-attacks.
## Usage
You can add `CsrfApp` to your own `App` struct to have the functions available
in your app's context, or you can use it with the middleware of vweb.
The advantage of the middleware approach is that you have to define
the configuration separate from your `App`. This makes it possible to share the
configuration between modules or controllers.
### Usage with the CsrfApp
Change `secret` and `allowed_hosts` when creating the `CsrfApp`.
**Example**:
```v ignore
module main
import net.http
import vweb
import vweb.csrf
struct App {
vweb.Context
pub mut:
csrf csrf.CsrfApp [vweb_global]
}
fn main() {
app := &App{
csrf: csrf.CsrfApp{
// change the secret
secret: 'my-64bytes-secret'
// change to which domains you want to allow
allowed_hosts: ['*']
}
}
vweb.run(app, 8080)
}
pub fn (mut app App) index() vweb.Result {
// this line sets `app.token` and the cookie
app.csrf.set_token(mut app.Context)
return $vweb.html()
}
[post]
pub fn (mut app App) auth() vweb.Result {
// this line protects the route against CSRF
app.csrf.protect(mut app.Context)
return app.text('authenticated!')
}
```
index.html
```html
<form action="/auth" method="post">
<input type="hidden" name="@app.csrf.token_name" value="@app.csrf.token"/>
<label for="password">Your password:</label>
<input type="text" id="password" name="password" placeholder="Your password" />
</form>
```
### Usage without CsrfApp
If you use `vweb.Middleware` you can protect multiple routes at once.
**Example**:
```v ignore
module main
import net.http
import vweb
import vweb.csrf
const (
// the configuration moved here
csrf_config = csrf.CsrfConfig{
// change the secret
secret: 'my-64bytes-secret'
// change to which domains you want to allow
allowed_hosts: ['*']
}
)
struct App {
vweb.Context
pub mut:
middlewares map[string][]vweb.Middleware
}
fn main() {
app := &App{
middlewares: {
// protect all routes starting with the url '/auth'
'/auth': [csrf.middleware(csrf_config)]
}
}
vweb.run(app, 8080)
}
pub fn (mut app App) index() vweb.Result {
// get the token and set the cookie
csrftoken := csrf.set_token(mut app.Context, csrf_config)
return $vweb.html()
}
[post]
pub fn (mut app App) auth() vweb.Result {
return app.text('authenticated!')
}
[post]
pub fn (mut app App) register() vweb.Result {
// protect an individual route with the following line
csrf.protect(mut app.Context, csrf_config)
// ...
}
```
index.html (the hidden input has changed)
```html
<form action="/auth" method="post">
<input type="hidden" name="@csrf_config.token_name" value="@csrftoken"/>
<label for="password">Your password:</label>
<input type="text" id="password" name="password" placeholder="Your password" />
</form>
```
### Protect all routes
It is possible to protect all routes against CSRF-attacks. Every request that is not
defined as a [safe method](#safe-methods) (`GET`, `OPTIONS`, `HEAD` by default)
will have CSRF-protection.
**Example**:
```v ignore
pub fn (mut app App) before_request() {
app.csrf.protect(mut app.Context)
// or if you don't use `CsrfApp`:
// csrf.protect(mut app.Context, csrf_config)
}
```
## How it works
This module implements the [double submit cookie][owasp] technique: a random token
is generated, the CSRF-token. The hmac of this token and the secret key is stored in a cookie.
When a request is made, the CSRF-token should be placed inside a HTML form element.
The CSRF-token the hmac of the CSRF-token in the formdata is compared to the cookie.
If the values match, the request is accepted.
This approach has the advantage of being stateless: there is no need to store tokens on the server
side and validate them. The token and cookie are bound cryptographically to each other so
an attacker would need to know both values in order to make a CSRF-attack succeed. That
is why is it important to **not leak the CSRF-token** via an url, or some other way.
See [client side CSRF][client-side-csrf] for more information.
This is a high level overview of the implementation.
## Security Considerations
### The secret key
The secret key should be a random string that is not easily guessable.
The recommended size is 64 bytes.
### Sessions
If your app supports some kind of user sessions, it is recommended to cryptographically
bind the CSRF-token to the users' session. You can do that by providing the name
of the session ID cookie. If an attacker changes the session ID in the cookie, in the
token or both the hmac will be different adn the request will be rejected.
**Example**:
```v ignore
csrf_config = csrf.CsrfConfig{
// ...
session_cookie: 'my_session_id_cookie_name'
}
```
### Safe Methods
The HTTP methods `GET`, `OPTIONS`, `HEAD` are considered
[safe methods][mozilla-safe-methods] meaning they should not alter the state of
an application. If a request with a "safe method" is made, the csrf protection will be skipped.
You can change which methods are considered safe by changing `CsrfConfig.safe_methods`.
### Allowed Hosts
By default, both the http Origin and Referer headers are checked and matched strictly
to the values in `allowed_hosts`. That means that you need to include each subdomain.
If the value of `allowed_hosts` contains the wildcard: `'*'` the headers will not be checked.
#### Domain name matching
The following configuration will not allow requests made from `test.example.com`,
only from `example.com`.
**Example**
```v ignore
config := csrf.CsrfConfig{
secret: '...'
allowed_hosts: ['example.com']
}
```
#### Referer, Origin header check
In some cases (like if your server is behind a proxy), the Origin or Referer header will
not be present. If that is your case you can set `check_origin_and_referer` to `false`.
Request will now be accepted when the Origin *or* Referer header is valid.
### Share csrf cookie with subdomains
If you need to share the CSRF-token cookie with subdomains, you can set
`same_site` to `.same_site_lax_mode`.
## Configuration
All configuration options are defined in `CsrfConfig`.
[//]: # (Sources)
[owasp]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
[client-side-csrf]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#client-side-csrf
[mozilla-safe-methods]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP

View File

@ -1,56 +0,0 @@
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 HttpOnly-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 throw an error.
pub fn (mut app App) get_csrf_token() ?string {
if app.csrf_cookie_value != '' {
return app.csrf_cookie_value
} else {
return CsrfError{
m: 'The CSRF-Token-Value is empty. Please check if you have setted a cookie!'
}
}
}

205
vlib/vweb/csrf/csrf.v Normal file
View File

@ -0,0 +1,205 @@
module csrf
import crypto.hmac
import crypto.sha256
import encoding.base64
import net.http
import net.urllib
import rand
import time
import vweb
[params]
pub struct CsrfConfig {
pub:
secret string
// how long the random part of the csrf-token should be
nonce_length int = 64
// HTTP "safe" methods meaning they shouldn't alter state.
// If a request with any of these methods is made, `protect` will always return true
// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
safe_methods []http.Method = [.get, .head, .options]
// which hosts are allowed, enforced by checking the Origin and Referer header
// if allowed_hosts contains '*' the check will be skipped.
// Subdomains need to be included separately: a request from `"sub.example.com"`
// will be rejected when `allowed_host = ['example.com']`.
allowed_hosts []string
// if set to true both the Referer and Origin headers must match `allowed_hosts`
// else if either one is valid the request is accepted
check_origin_and_referer bool = true
// the name of the csrf-token in the hidden html input
token_name string = 'csrftoken'
// the name of the cookie that contains the session id
session_cookie string
// cookie options
cookie_name string = 'csrftoken'
same_site http.SameSite = .same_site_strict_mode
cookie_path string = '/'
// how long the cookie stays valid in seconds. Default is 30 days
max_age int = 60 * 60 * 24 * 30
cookie_domain string
}
pub struct CsrfApp {
CsrfConfig
pub mut:
// the csrftoken that should be placed in an html form
token string
}
// set_token is the app wrapper for `set_token`
pub fn (mut app CsrfApp) set_token(mut ctx vweb.Context) {
app.token = set_token(mut ctx, app.CsrfConfig)
}
// protect is the app wrapper for `protect`
pub fn (mut app CsrfApp) protect(mut ctx vweb.Context) bool {
return protect(mut ctx, app.CsrfConfig)
}
// check_origin_and_referer is the app wrapper for `check_origin_and_referer`
fn (app &CsrfApp) check_origin_and_referer(ctx vweb.Context) bool {
return check_origin_and_referer(ctx, app.CsrfConfig)
}
// middleware returns a function that you can use in `app.middlewares`
pub fn middleware(config &CsrfConfig) vweb.Middleware {
return fn [config] (mut ctx vweb.Context) bool {
return protect(mut ctx, config)
}
}
// set_token returns the csrftoken and sets an encrypted cookie with the hmac of
// `config.get_secret` and the csrftoken
pub fn set_token(mut ctx vweb.Context, config &CsrfConfig) string {
expire_time := time.now().add_seconds(config.max_age)
session_id := ctx.get_cookie(config.session_cookie) or { '' }
token := generate_token(expire_time.unix_time(), session_id, config.nonce_length)
cookie := generate_cookie(expire_time.unix_time(), token, config.secret)
// the hmac key is set as a cookie and later validated with `app.token` that must
// be in an html form
ctx.set_cookie(http.Cookie{
name: config.cookie_name
value: cookie
same_site: config.same_site
http_only: true
secure: true
path: config.cookie_path
expires: expire_time
max_age: config.max_age
})
return token
}
// protect returns false and sends an http 401 response when the csrf verification
// fails. protect will always return true if the current request method is in
// `config.safe_methods`.
pub fn protect(mut ctx vweb.Context, config &CsrfConfig) bool {
// if the request method is a "safe" method we allow the request
if ctx.req.method in config.safe_methods {
return true
}
// check origin and referer header
if check_origin_and_referer(ctx, config) == false {
request_is_invalid(mut ctx)
return false
}
// use the session id from the cookie, not from the csrftoken
session_id := ctx.get_cookie(config.session_cookie) or { '' }
actual_token := ctx.form[config.token_name] or {
request_is_invalid(mut ctx)
return false
}
// retrieve timestamp and nonce from csrftoken
data := base64.url_decode_str(actual_token).split('.')
if data.len < 3 {
request_is_invalid(mut ctx)
return false
}
// check the timestamp from the csrftoken against the current time
// if an attacker would change the timestamp on the cookie, the token or both the
// hmac would also change.
now := time.now().unix_time()
expire_timestamp := data[0].i64()
if expire_timestamp < now {
// token has expired
request_is_invalid(mut ctx)
return false
}
nonce := data.last()
expected_token := base64.url_encode_str('${expire_timestamp}.${session_id}.${nonce}')
actual_hash := ctx.get_cookie(config.cookie_name) or {
request_is_invalid(mut ctx)
return false
}
// generate new hmac based on information in the http request
expected_hash := generate_cookie(expire_timestamp, expected_token, config.secret)
// if the new hmac matches the cookie value the request is legit
if actual_hash != expected_hash {
request_is_invalid(mut ctx)
return false
}
return true
}
// check_origin_and_referer validates the `Origin` and `Referer` headers.
fn check_origin_and_referer(ctx vweb.Context, config &CsrfConfig) bool {
// wildcard allow all hosts NOT SAFE!
if '*' in config.allowed_hosts {
return true
}
// only match host and match the full domain name
// because lets say `allowed_host` = `['example.com']`.
// Attackers shouldn't be able to bypass this check with the domain `example.com.attacker.com`
origin := ctx.get_header('Origin')
origin_url := urllib.parse(origin) or { urllib.URL{} }
valid_origin := origin_url.hostname() in config.allowed_hosts
referer := ctx.get_header('Referer')
referer_url := urllib.parse(referer) or { urllib.URL{} }
valid_referer := referer_url.hostname() in config.allowed_hosts
if config.check_origin_and_referer {
return valid_origin && valid_referer
} else {
return valid_origin || valid_referer
}
}
// request_is_invalid sends an http 403 response
fn request_is_invalid(mut ctx vweb.Context) {
ctx.set_status(403, '')
ctx.text('Forbidden: Invalid or missing CSRF token')
}
fn generate_token(expire_time i64, session_id string, nonce_length int) string {
nonce := rand.string_from_set('0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz',
nonce_length)
token := '${expire_time}.${session_id}.${nonce}'
return base64.url_encode_str(token)
}
// generate_cookie converts secret key based on the request context and a random
// token into an hmac key
fn generate_cookie(expire_time i64, token string, secret string) string {
hash := base64.url_encode(hmac.new(secret.bytes(), token.bytes(), sha256.sum, sha256.block_size))
cookie := '${expire_time}.${hash}'
return cookie
}

View File

@ -1,30 +1,377 @@
import time
import net.http
import net.html
import vweb
import vweb.csrf
import os
const sport = 10801
const (
sport = 12385
localserver = '127.0.0.1:${sport}'
exit_after_time = 12000 // milliseconds
session_id_cookie_name = 'session_id'
csrf_config = &csrf.CsrfConfig{
secret: 'my-256bit-secret'
allowed_hosts: ['*']
session_cookie: session_id_cookie_name
}
allowed_origin = 'example.com'
csrf_config_origin = &csrf.CsrfConfig{
secret: 'my-256bit-secret'
allowed_hosts: [allowed_origin]
session_cookie: session_id_cookie_name
}
)
// Test CSRF functions
// =====================================
fn test_set_token() {
mut ctx := vweb.Context{}
token := csrf.set_token(mut ctx, csrf_config)
cookie := ctx.header.get(.set_cookie) or { '' }
assert cookie.len != 0
assert cookie.starts_with('${csrf_config.cookie_name}=')
}
fn test_protect() {
mut ctx := vweb.Context{}
token := csrf.set_token(mut ctx, csrf_config)
mut cookie := ctx.header.get(.set_cookie) or { '' }
// get cookie value from "name=value;"
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
form := {
csrf_config.token_name: token
}
cookie_map := {
csrf_config.cookie_name: cookie
}
ctx = vweb.Context{
form: form
req: http.Request{
method: .post
cookies: cookie_map
}
}
valid := csrf.protect(mut ctx, csrf_config)
assert valid == true
}
fn test_timeout() {
timeout := 1
short_time_config := &csrf.CsrfConfig{
secret: 'my-256bit-secret'
allowed_hosts: ['*']
session_cookie: session_id_cookie_name
max_age: timeout
}
mut ctx := vweb.Context{}
token := csrf.set_token(mut ctx, short_time_config)
// after 2 seconds the cookie should expire (maxage)
time.sleep(2 * time.second)
mut cookie := ctx.header.get(.set_cookie) or { '' }
// get cookie value from "name=value;"
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
form := {
short_time_config.token_name: token
}
cookie_map := {
short_time_config.cookie_name: cookie
}
ctx = vweb.Context{
form: form
req: http.Request{
method: .post
cookies: cookie_map
}
}
valid := csrf.protect(mut ctx, short_time_config)
assert valid == false
}
fn test_valid_origin() {
// valid because both Origin and Referer headers are present
token, cookie := get_token_cookie('')
form := {
csrf_config.token_name: token
}
cookie_map := {
csrf_config.cookie_name: cookie
}
mut req := http.Request{
method: .post
cookies: cookie_map
}
req.add_header(.origin, 'http://${allowed_origin}')
req.add_header(.referer, 'http://${allowed_origin}/test')
mut ctx := vweb.Context{
form: form
req: req
}
mut valid := csrf.protect(mut ctx, csrf_config_origin)
assert valid == true
}
fn test_invalid_origin() {
// invalid because either the Origin, Referer or neither are present
token, cookie := get_token_cookie('')
form := {
csrf_config.token_name: token
}
cookie_map := {
csrf_config.cookie_name: cookie
}
mut req := http.Request{
method: .post
cookies: cookie_map
}
req.add_header(.origin, 'http://${allowed_origin}')
mut ctx := vweb.Context{
form: form
req: req
}
mut valid := csrf.protect(mut ctx, csrf_config_origin)
assert valid == false
req = http.Request{
method: .post
cookies: cookie_map
}
req.add_header(.referer, 'http://${allowed_origin}/test')
ctx = vweb.Context{
form: form
req: req
}
valid = csrf.protect(mut ctx, csrf_config_origin)
assert valid == false
req = http.Request{
method: .post
cookies: cookie_map
}
ctx = vweb.Context{
form: form
req: req
}
valid = csrf.protect(mut ctx, csrf_config_origin)
assert valid == false
}
// Testing App
// ================================
struct App {
csrf.App
vweb.Context
pub mut:
csrf csrf.CsrfApp [vweb_global]
middlewares map[string][]vweb.Middleware
}
// 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}")
pub fn (mut app App) index() vweb.Result {
app.csrf.set_token(mut app.Context)
return app.html('<form action="/auth" method="post">
<input type="hidden" name="${app.csrf.token_name}" value="${app.csrf.token}"/>
<label for="password">Your password:</label>
<input type="text" id="password" name="password" placeholder="Your password" />
</form>')
}
fn test_send_a_request_to_homepage_expecting_a_csrf_cookie() {
spawn 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
[post]
pub fn (mut app App) auth() vweb.Result {
app.csrf.protect(mut app.Context)
return app.ok('authenticated')
}
pub fn (mut app App) middleware_index() vweb.Result {
csrftoken := csrf.set_token(mut app.Context, csrf_config)
return app.html('<form action="/auth" method="post">
<input type="hidden" name="${csrf_config.token_name}" value="${csrftoken}"/>
<label for="password">Your password:</label>
<input type="text" id="password" name="password" placeholder="Your password" />
</form>')
}
[post]
pub fn (mut app App) middleware_auth() vweb.Result {
return app.ok('middleware authenticated')
}
// App cleanup function
// ======================================
pub fn (mut app App) shutdown() vweb.Result {
spawn app.gracefull_exit()
return app.ok('good bye')
}
fn (mut app App) gracefull_exit() {
eprintln('>> webserver: gracefull_exit')
time.sleep(100 * time.millisecond)
exit(0)
}
fn exit_after_timeout[T](mut app T, timeout_in_ms int) {
time.sleep(timeout_in_ms * time.millisecond)
eprintln('>> webserver: pid: ${os.getpid()}, exiting ...')
app.shutdown()
eprintln('App timed out!')
assert true == false
}
// Tests for the App
// ======================================
fn test_run_app_in_background() {
mut app := &App{
csrf: csrf.CsrfApp{
secret: 'my-256bit-secret'
allowed_hosts: [allowed_origin]
session_cookie: session_id_cookie_name
}
middlewares: {
'/middleware_auth': [csrf.middleware(csrf_config)]
}
}
spawn vweb.run_at(app, port: sport, family: .ip)
spawn exit_after_timeout(mut app, exit_after_time)
time.sleep(500 * time.millisecond)
}
fn test_token_with_app() {
res := http.get('http://${localserver}/') or { panic(err) }
mut doc := html.parse(res.body)
inputs := doc.get_tag_by_attribute_value('type', 'hidden')
assert inputs.len == 1
assert csrf_config.token_name == inputs[0].attributes['name']
}
fn test_token_with_middleware() {
res := http.get('http://${localserver}/middleware_index') or { panic(err) }
mut doc := html.parse(res.body)
inputs := doc.get_tag_by_attribute_value('type', 'hidden')
assert inputs.len == 1
assert csrf_config.token_name == inputs[0].attributes['name']
}
// utility function to check whether the route at `path` is protected against csrf
fn protect_route_util(path string) {
mut req := http.Request{
method: .post
url: 'http://${localserver}/${path}'
}
mut res := req.do() or { panic(err) }
assert res.status() == .forbidden
// A valid request with CSRF protection should have a cookie session id,
// csrftoken in `app.form` and the hmac of that token in a cookie
session_id := 'user_session_id'
token, cookie := get_token_cookie(session_id)
header := http.new_header_from_map({
http.CommonHeader.origin: 'http://${allowed_origin}'
http.CommonHeader.referer: 'http://${allowed_origin}/route'
})
formdata := http.url_encode_form_data({
csrf_config.token_name: token
})
// session id is altered: test if session hijacking is possible
// if the session id the csrftoken changes so the cookie can't be validated
mut cookies := {
csrf_config.cookie_name: cookie
session_id_cookie_name: 'altered'
}
req = http.Request{
method: .post
url: 'http://${localserver}/${path}'
data: formdata
cookies: cookies
header: header
}
res = req.do() or { panic(err) }
assert res.status() == .forbidden
// Everything is valid now and the request should succeed
cookies[session_id_cookie_name] = session_id
req = http.Request{
method: .post
url: 'http://${localserver}/${path}'
data: formdata
cookies: cookies
header: header
}
res = req.do() or { panic(err) }
assert res.status() == .ok
}
fn test_protect_with_app() {
protect_route_util('/auth')
}
fn test_protect_with_middleware() {
protect_route_util('middleware_auth')
}
fn testsuite_end() {
// This test is guaranteed to be called last.
// It sends a request to the server to shutdown.
x := http.get('http://${localserver}/shutdown') or {
assert err.msg() == ''
return
}
assert x.status() == .ok
assert x.body == 'good bye'
}
// Utility functions
fn get_token_cookie(session_id string) (string, string) {
mut ctx := vweb.Context{
req: http.Request{
cookies: {
session_id_cookie_name: session_id
}
}
}
token := csrf.set_token(mut ctx, csrf_config_origin)
mut cookie := ctx.header.get(.set_cookie) or { '' }
// get cookie value from "name=value;"
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
return token, cookie
}

View File

@ -1,37 +0,0 @@
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
}
}

View File

@ -1,30 +0,0 @@
// 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