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
|
# 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 time
|
||||||
import net.http
|
import net.http
|
||||||
|
import net.html
|
||||||
import vweb
|
import vweb
|
||||||
import vweb.csrf
|
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 {
|
struct App {
|
||||||
csrf.App
|
vweb.Context
|
||||||
|
pub mut:
|
||||||
|
csrf csrf.CsrfApp [vweb_global]
|
||||||
|
middlewares map[string][]vweb.Middleware
|
||||||
}
|
}
|
||||||
|
|
||||||
// index - will handle requests to path '/'
|
pub fn (mut app App) index() vweb.Result {
|
||||||
fn (mut app App) index() vweb.Result {
|
app.csrf.set_token(mut app.Context)
|
||||||
// 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})
|
return app.html('<form action="/auth" method="post">
|
||||||
// Get the token-value from the csrf-cookie that was just setted
|
<input type="hidden" name="${app.csrf.token_name}" value="${app.csrf.token}"/>
|
||||||
token := app.get_csrf_token() or { panic(err) }
|
<label for="password">Your password:</label>
|
||||||
return app.text("Csrf-Token set! It's value is: ${token}")
|
<input type="text" id="password" name="password" placeholder="Your password" />
|
||||||
|
</form>')
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_send_a_request_to_homepage_expecting_a_csrf_cookie() {
|
[post]
|
||||||
spawn vweb.run_at(&App{}, vweb.RunParams{ port: sport })
|
pub fn (mut app App) auth() vweb.Result {
|
||||||
time.sleep(500 * time.millisecond)
|
app.csrf.protect(mut app.Context)
|
||||||
res := http.get('http://localhost:${sport}/')!
|
|
||||||
if res.header.str().contains('__Host-Csrf-Token') {
|
return app.ok('authenticated')
|
||||||
assert true
|
}
|
||||||
} else {
|
|
||||||
assert false
|
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…
x
Reference in New Issue
Block a user