1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
let log_src = Logs.Src.create "sihl.middleware.csrf"
module Logs = (val Logs.src_log log_src : Logs.LOG)
let key : string Opium.Context.key =
Opium.Context.Key.create ("csrf token", Sexplib.Std.sexp_of_string)
;;
let find req = Opium.Context.find key req.Opium.Request.env
let set token req =
let env = req.Opium.Request.env in
let env = Opium.Context.add key token env in
{ req with env }
;;
let default_not_allowed_handler _ =
Opium.Response.(of_plain_text ~status:`Forbidden "") |> Lwt.return
;;
let hash ~with_secret value =
value
|> Cstruct.of_string
|> Mirage_crypto.Hash.mac `SHA1 ~key:(Cstruct.of_string with_secret)
|> Cstruct.to_string
|> Base64.encode_exn
;;
let verify ~with_secret ~hashed value =
String.equal hashed (hash ~with_secret value)
;;
let middleware
?(not_allowed_handler = default_not_allowed_handler)
?(cookie_key = "__Host-csrf")
?(input_name = "_csrf")
?(secret = Core_configuration.read_secret ())
()
=
let filter handler req =
let check_csrf =
Core_configuration.is_production ()
|| Option.value (Core_configuration.read_bool "CHECK_CSRF") ~default:false
in
if not check_csrf
then
handler (set "development" req)
else (
let%lwt token = Opium.Request.urlencoded input_name req in
let new_token = Core_random.base64 80 in
let req = set new_token req in
let construct_response handler =
handler req
|> Lwt.map
@@ Opium.Response.add_cookie_or_replace
~scope:(Uri.of_string "/")
~secure:true
(cookie_key, hash ~with_secret:secret new_token)
in
let is_safe =
match req.Opium.Request.meth with
| `GET | `HEAD | `OPTIONS | `TRACE -> true
| _ -> false
in
match token, is_safe with
| _, true -> construct_response handler
| None, false -> construct_response not_allowed_handler
| Some received_token, false ->
let stored_token = Opium.Request.cookie cookie_key req in
(match stored_token with
| None ->
Logs.err (fun m ->
m "No token stored for CSRF token '%s'" received_token);
construct_response not_allowed_handler
| Some stored_token ->
if verify ~with_secret:secret ~hashed:stored_token received_token
then construct_response handler
else (
Logs.err (fun m ->
m
"Hashed stored token '%s' does not match with the hashed \
received token '%s'"
stored_token
received_token);
construct_response not_allowed_handler)))
in
Rock.Middleware.create ~name:"csrf" ~filter
;;