Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,254 @@
|
||||
# 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
|
||||
|
||||
To enable CSRF-protection for your veb app you must embed the `CsrfContext` struct
|
||||
on your `Context` struct. You must also provide configuration options
|
||||
(see [configuration & security](#configuration--security-considerations)).
|
||||
|
||||
**Example:**
|
||||
|
||||
```v
|
||||
import veb
|
||||
import veb.csrf
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
csrf.CsrfContext
|
||||
}
|
||||
```
|
||||
|
||||
Change `secret` and `allowed_hosts` in a production environment!
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
const csrf_config := csrf.CsrfConfig{
|
||||
secret: 'my-secret'
|
||||
allowed_hosts: ['*']
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
Enable CSRF protection for all routes, or a certain route(s) by using veb's middleware.
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub struct App {
|
||||
veb.Middleware[Context]
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
// register the CSRF middleware and pass our configuration
|
||||
// protect a specific route
|
||||
app.route_use('/login', csrf.middleware[Context](csrf_config))
|
||||
veb.run[App, Context](mut app, 8080)
|
||||
}
|
||||
```
|
||||
|
||||
### Setting the token
|
||||
|
||||
For the CSRF-protection to work we have to generate an anti-CSRF token and set it
|
||||
as an hidden input field on any form that will be submitted to the route we
|
||||
want to protect.
|
||||
|
||||
**Example:**
|
||||
_main.v_
|
||||
|
||||
```v ignore
|
||||
fn (app &App) index(mut ctx) veb.Result {
|
||||
// assign the config before generating the token on non-middleware routes
|
||||
ctx.config = csrf_config
|
||||
// this function will set a cookie header and generate a CSRF token
|
||||
ctx.set_csrf_token(mut ctx)
|
||||
return $veb.html()
|
||||
}
|
||||
|
||||
@[post]
|
||||
fn (app &App) login(mut ctx, password string) veb.Result {
|
||||
// implement your own password validation here
|
||||
if password == 'password' {
|
||||
return ctx.text('You are logged in!')
|
||||
} else {
|
||||
return ctx.text('Invalid password!')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_templates/index.html_
|
||||
|
||||
```html
|
||||
<h1>Log in</h1>
|
||||
<form method="POST" action="/login">
|
||||
@{ctx.csrf_token_input()}
|
||||
<label for="password">Password:</label>
|
||||
<input type="text" name="password" id="password" />
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
If we run the app with `v run main.v` and navigate to `http://localhost:8080/`
|
||||
we will see the login form and we can login using the password "password".
|
||||
|
||||
If we remove the hidden input, by removing the line `@{ctx.csrf_token_input()}`
|
||||
from our html code we will see an error message indicating that the CSRF token
|
||||
is not set or invalid! By default the CSRF module sends an HTTP-403 response when
|
||||
a token is invalid, if you want to send a custom response see the
|
||||
[advanced usage](#advanced-usage) section.
|
||||
|
||||
> **Note:**
|
||||
> Please read the security and configuration section! If you configure
|
||||
> the CSRF module in an unsafe way, the protection will be useless.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
If you want more control over what routes are protected or what action you want to
|
||||
do when a CSRF-token is invalid, you can call `csrf.protect` yourself whenever you want
|
||||
to protect a route against CSRF attacks. This function returns `false` if the current CSRF token
|
||||
and cookie combination is not valid.
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
@[post]
|
||||
fn (app &App) login(mut ctx, password string) veb.Result {
|
||||
if csrf.protect(mut ctx, csrf_config) == false {
|
||||
// CSRF verification failed!
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Obtaining the anti-CSRF token
|
||||
|
||||
When `set_csrf_token` is called the token is stored in the `csrf_token` field. You access
|
||||
this field directly to use it in an input field, or call `csrf_token_input`.
|
||||
If the handler is not covered by `csrf.middleware`, assign `ctx.config = csrf_config`
|
||||
first so the generated cookie and token use the same configuration.
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
fn (app &App) index(mut ctx) veb.Result {
|
||||
ctx.config = csrf_config
|
||||
token := ctx.set_csrf_token(mut ctx)
|
||||
}
|
||||
```
|
||||
|
||||
### Clearing the anti-CSRF token
|
||||
|
||||
If you want to remove the anti-CSRF token and the cookie header you can call `clear_csrf_token`
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
ctx.clear_csrf_token()
|
||||
```
|
||||
|
||||
## 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. This is way
|
||||
by default the `HTTPOnly` flag on the cookie is set to true.
|
||||
See [client side CSRF][client-side-csrf] for more information.
|
||||
|
||||
This is a high level overview of the implementation.
|
||||
|
||||
## Configuration & Security Considerations
|
||||
|
||||
### The secret key
|
||||
|
||||
The secret key should be a random string that is not easily guessable.
|
||||
|
||||
### 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 and 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
|
||||
@@ -0,0 +1,241 @@
|
||||
module csrf
|
||||
|
||||
import crypto.hmac
|
||||
import crypto.sha256
|
||||
import encoding.base64
|
||||
import net.http
|
||||
import net.urllib
|
||||
import rand
|
||||
import time
|
||||
import veb
|
||||
|
||||
@[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
|
||||
// whether the cookie can be send only over HTTPS
|
||||
secure bool
|
||||
// enable printing verbose statements
|
||||
verbose bool
|
||||
}
|
||||
|
||||
pub struct CsrfContext {
|
||||
pub mut:
|
||||
config CsrfConfig
|
||||
exempt bool
|
||||
// the csrftoken that should be placed in an html form
|
||||
csrf_token string
|
||||
}
|
||||
|
||||
// set_token generates a new csrf_token and adds a Cookie to the response
|
||||
pub fn (mut ctx CsrfContext) set_csrf_token[T](mut user_context T) string {
|
||||
ctx.csrf_token = set_token(mut user_context, ctx.config)
|
||||
return ctx.csrf_token
|
||||
}
|
||||
|
||||
// clear the csrf token and cookie header from the context
|
||||
pub fn (ctx &CsrfContext) clear_csrf_token[T](mut user_context T) {
|
||||
user_context.set_cookie(http.Cookie{
|
||||
name: config.cookie_name
|
||||
value: ''
|
||||
max_age: 0
|
||||
})
|
||||
}
|
||||
|
||||
// csrf_token_input returns an HTML hidden input containing the csrf token
|
||||
pub fn (ctx &CsrfContext) csrf_token_input() veb.RawHtml {
|
||||
return '<input type="hidden" name="${ctx.config.token_name}" value="${ctx.csrf_token}">'
|
||||
}
|
||||
|
||||
// middleware returns a handler that you can use with veb's middleware
|
||||
pub fn middleware[T](config CsrfConfig) veb.MiddlewareOptions[T] {
|
||||
return veb.MiddlewareOptions[T]{
|
||||
after: false
|
||||
handler: fn [config] [T](mut ctx T) bool {
|
||||
ctx.config = config
|
||||
if ctx.exempt {
|
||||
return true
|
||||
} else if ctx.req.method in config.safe_methods {
|
||||
return true
|
||||
} else {
|
||||
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 veb.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(), session_id, config.nonce_length)
|
||||
cookie := generate_cookie(expire_time.unix(), 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: config.secure
|
||||
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 veb.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 config.verbose {
|
||||
eprintln('[CSRF] Token data: ${data}')
|
||||
}
|
||||
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()
|
||||
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}')
|
||||
|
||||
mut actual_hash := ctx.get_cookie(config.cookie_name) or {
|
||||
request_is_invalid(mut ctx)
|
||||
return false
|
||||
}
|
||||
// old_expire := actual_hash.all_before('.')
|
||||
// actual_hash = actual_hash.replace_once (old_expire, expire_timestamp.str())
|
||||
|
||||
// generate new hmac based on information in the http request
|
||||
expected_hash := generate_cookie(expire_timestamp, expected_token, config.secret)
|
||||
if config.verbose {
|
||||
eprintln('[CSRF] Actual Hash: ${actual_hash}')
|
||||
eprintln('[CSRF] Expected Hash: ${expected_hash}')
|
||||
}
|
||||
|
||||
// if the new hmac matches the cookie value the request is legit
|
||||
if actual_hash != expected_hash {
|
||||
if config.verbose {
|
||||
eprintln('[CSRF] The actual hash differs from the expected hash')
|
||||
}
|
||||
request_is_invalid(mut ctx)
|
||||
return false
|
||||
}
|
||||
if config.verbose {
|
||||
eprintln('[CSRF] The actual hash matches the expected hash')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// check_origin_and_referer validates the `Origin` and `Referer` headers.
|
||||
fn check_origin_and_referer(ctx veb.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) or { return false }
|
||||
origin_url := urllib.parse(origin) or { urllib.URL{} }
|
||||
|
||||
valid_origin := origin_url.hostname() in config.allowed_hosts
|
||||
|
||||
referer := ctx.get_header(.referer) or { return false }
|
||||
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 veb.Context) {
|
||||
ctx.res.set_status(.forbidden)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// vtest build: !windows // fasthttp.Server.run is not implemented on windows yet
|
||||
import time
|
||||
import net.http
|
||||
import net.html
|
||||
import os
|
||||
import veb
|
||||
import veb.csrf
|
||||
|
||||
const sport = 12385
|
||||
const localserver = '127.0.0.1:${sport}'
|
||||
const exit_after_time = 12000 // milliseconds
|
||||
|
||||
const session_id_cookie_name = 'session_id'
|
||||
const csrf_config = &csrf.CsrfConfig{
|
||||
secret: 'my-256bit-secret'
|
||||
allowed_hosts: ['*']
|
||||
session_cookie: session_id_cookie_name
|
||||
}
|
||||
|
||||
const allowed_origin = 'example.com'
|
||||
const 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 := veb.Context{}
|
||||
|
||||
token := csrf.set_token(mut ctx, csrf_config)
|
||||
|
||||
cookie := ctx.res.header.get(.set_cookie) or { '' }
|
||||
assert cookie.len != 0
|
||||
assert cookie.starts_with('${csrf_config.cookie_name}=')
|
||||
}
|
||||
|
||||
fn test_protect() {
|
||||
mut ctx := veb.Context{}
|
||||
|
||||
token := csrf.set_token(mut ctx, csrf_config)
|
||||
|
||||
mut cookie := ctx.res.header.get(.set_cookie) or { '' }
|
||||
// get cookie value from "name=value;"
|
||||
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
|
||||
|
||||
ctx = veb.Context{
|
||||
form: {
|
||||
csrf_config.token_name: token
|
||||
}
|
||||
req: http.Request{
|
||||
method: .post
|
||||
}
|
||||
}
|
||||
ctx.req.add_cookie(name: csrf_config.cookie_name, value: cookie)
|
||||
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 := veb.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.res.header.get(.set_cookie) or { '' }
|
||||
// get cookie value from "name=value;"
|
||||
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
|
||||
|
||||
ctx = veb.Context{
|
||||
form: {
|
||||
short_time_config.token_name: token
|
||||
}
|
||||
req: http.Request{
|
||||
method: .post
|
||||
}
|
||||
}
|
||||
ctx.req.add_cookie(name: short_time_config.cookie_name, value: cookie)
|
||||
|
||||
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('')
|
||||
|
||||
mut req := http.Request{
|
||||
method: .post
|
||||
}
|
||||
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
|
||||
req.add_header(.origin, 'http://${allowed_origin}')
|
||||
req.add_header(.referer, 'http://${allowed_origin}/test')
|
||||
mut ctx := veb.Context{
|
||||
form: {
|
||||
csrf_config.token_name: token
|
||||
}
|
||||
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('')
|
||||
|
||||
mut req := http.Request{
|
||||
method: .post
|
||||
}
|
||||
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
|
||||
req.add_header(.origin, 'http://${allowed_origin}')
|
||||
mut ctx := veb.Context{
|
||||
form: {
|
||||
csrf_config.token_name: token
|
||||
}
|
||||
req: req
|
||||
}
|
||||
|
||||
mut valid := csrf.protect(mut ctx, csrf_config_origin)
|
||||
assert valid == false
|
||||
|
||||
req = http.Request{
|
||||
method: .post
|
||||
}
|
||||
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
|
||||
req.add_header(.referer, 'http://${allowed_origin}/test')
|
||||
ctx = veb.Context{
|
||||
form: {
|
||||
csrf_config.token_name: token
|
||||
}
|
||||
req: req
|
||||
}
|
||||
|
||||
valid = csrf.protect(mut ctx, csrf_config_origin)
|
||||
assert valid == false
|
||||
|
||||
req = http.Request{
|
||||
method: .post
|
||||
}
|
||||
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
|
||||
ctx = veb.Context{
|
||||
form: {
|
||||
csrf_config.token_name: token
|
||||
}
|
||||
req: req
|
||||
}
|
||||
|
||||
valid = csrf.protect(mut ctx, csrf_config_origin)
|
||||
assert valid == false
|
||||
}
|
||||
|
||||
// Testing App
|
||||
// ================================
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
csrf.CsrfContext
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
veb.Middleware[Context]
|
||||
mut:
|
||||
started chan bool
|
||||
}
|
||||
|
||||
pub fn (mut app App) before_accept_loop() {
|
||||
app.started <- true
|
||||
}
|
||||
|
||||
fn (app &App) index(mut ctx Context) veb.Result {
|
||||
ctx.config = *csrf_config
|
||||
ctx.set_csrf_token(mut ctx)
|
||||
|
||||
return ctx.html('<form action="/auth" method="post">
|
||||
${ctx.csrf_token_input()}
|
||||
<label for="password">Your password:</label>
|
||||
<input type="text" id="password" name="password" placeholder="Your password" />
|
||||
</form>')
|
||||
}
|
||||
|
||||
@[post]
|
||||
fn (app &App) auth(mut ctx Context) veb.Result {
|
||||
return ctx.ok('authenticated')
|
||||
}
|
||||
|
||||
// App cleanup function
|
||||
// ======================================
|
||||
|
||||
pub fn (mut app App) shutdown(mut ctx Context) veb.Result {
|
||||
spawn app.exit_gracefully()
|
||||
return ctx.ok('good bye')
|
||||
}
|
||||
|
||||
fn (app &App) exit_gracefully() {
|
||||
eprintln('>> webserver: exit_gracefully')
|
||||
time.sleep(100 * time.millisecond)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
fn exit_after_timeout(mut app App, timeout_in_ms int) {
|
||||
time.sleep(timeout_in_ms * time.millisecond)
|
||||
eprintln('>> webserver: pid: ${os.getpid()}, exiting ...')
|
||||
app.exit_gracefully()
|
||||
|
||||
eprintln('App timed out!')
|
||||
assert true == false
|
||||
}
|
||||
|
||||
// Tests for the App
|
||||
// ======================================
|
||||
|
||||
fn test_run_app_in_background() {
|
||||
mut app := &App{}
|
||||
app.route_use('/auth', csrf.middleware[Context](csrf_config))
|
||||
|
||||
spawn exit_after_timeout(mut app, exit_after_time)
|
||||
spawn veb.run_at[App, Context](mut app, port: sport, family: .ip)
|
||||
_ := <-app.started
|
||||
}
|
||||
|
||||
fn test_token_input() {
|
||||
res := http.get('http://${localserver}/') or { panic(err) }
|
||||
|
||||
mut doc := html.parse(res.body)
|
||||
inputs := doc.get_tags_by_attribute_value('type', 'hidden')
|
||||
assert inputs.len == 1
|
||||
assert csrf_config.token_name == inputs[0].attributes['name']
|
||||
}
|
||||
|
||||
fn test_token_roundtrip_from_index_form() {
|
||||
res := http.get('http://${localserver}/') or { panic(err) }
|
||||
assert res.status() == .ok
|
||||
|
||||
cookies := res.cookies()
|
||||
assert cookies.len == 1
|
||||
|
||||
mut doc := html.parse(res.body)
|
||||
inputs := doc.get_tags_by_attribute_value('type', 'hidden')
|
||||
assert inputs.len == 1
|
||||
|
||||
token := inputs[0].attributes['value']
|
||||
assert token.len != 0
|
||||
|
||||
mut req := http.Request{
|
||||
method: .post
|
||||
url: 'http://${localserver}/auth'
|
||||
data: http.url_encode_form_data({
|
||||
csrf_config.token_name: token
|
||||
})
|
||||
}
|
||||
for cookie in cookies {
|
||||
req.add_cookie(cookie)
|
||||
}
|
||||
|
||||
post_res := req.do() or { panic(err) }
|
||||
assert post_res.status() == .ok
|
||||
assert post_res.body == 'authenticated'
|
||||
}
|
||||
|
||||
// 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
|
||||
header: header
|
||||
}
|
||||
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
|
||||
req.add_cookie(name: session_id_cookie_name, value: 'altered')
|
||||
|
||||
res = req.do() or { panic(err) }
|
||||
assert res.status() == .forbidden
|
||||
|
||||
req = http.Request{
|
||||
method: .post
|
||||
url: 'http://${localserver}/${path}'
|
||||
data: formdata
|
||||
header: header
|
||||
}
|
||||
req.add_cookie(name: csrf_config.cookie_name, value: cookie)
|
||||
req.add_cookie(name: session_id_cookie_name, value: session_id)
|
||||
|
||||
// Everything is valid now and the request should succeed, since session_id_cookie_name will be session_id
|
||||
res = req.do() or { panic(err) }
|
||||
assert res.status() == .ok
|
||||
}
|
||||
|
||||
fn test_protect_app() {
|
||||
protect_route_util('/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 := veb.Context{}
|
||||
ctx.req.add_cookie(name: session_id_cookie_name, value: session_id)
|
||||
|
||||
token := csrf.set_token(mut ctx, csrf_config_origin)
|
||||
|
||||
mut cookie := ctx.res.header.get(.set_cookie) or { '' }
|
||||
// get cookie value from "name=value;"
|
||||
cookie = cookie.split(' ')[0].all_after('=').replace(';', '')
|
||||
return token, cookie
|
||||
}
|
||||
Reference in new issue
Block a user