1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00
v/vlib/vweb/csrf/csrf.v
2023-05-21 01:56:17 +03:00

206 lines
6.4 KiB
V

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
}