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:
parent
adcf47dcce
commit
d0214a254e
@ -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
237
vlib/vweb/csrf/README.md
Normal 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
|
@ -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
205
vlib/vweb/csrf/csrf.v
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
Loading…
Reference in New Issue
Block a user