package sihl

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file middleware_csrf.ml

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
module Core = Sihl_core
module Utils = Sihl_utils
module Http = Sihl_http
module Token = Sihl_token
module Session = Sihl_session
open Lwt.Syntax

let log_src = Logs.Src.create ~doc:"CSRF Middleware" "sihl.middleware.csrf"

module Logs = (val Logs.src_log log_src : Logs.LOG)

let key : string Opium_kernel.Hmap.key =
  Opium_kernel.Hmap.Key.create ("csrf token", Sexplib.Std.sexp_of_string)
;;

exception Crypto_failed of string

(* Can be used to fetch token in view for forms *)
let find req = Opium_kernel.Hmap.find_exn key (Opium_kernel.Request.env req)

let find_opt req =
  try Some (find req) with
  | _ -> None
;;

let set token req =
  let env = Opium_kernel.Request.env req in
  let env = Opium_kernel.Hmap.add key token env in
  { req with env }
;;

(* TODO (https://docs.djangoproject.com/en/3.0/ref/csrf/#how-it-works) Check other Django
   specifics namely:
 * Testing views with custom HTTP client
 * Allow Sihl user to make views exempt
 * Enable subdomain
 * HTML caching token handling
 *)
module Make (TokenService : Token.Sig.SERVICE) (SessionService : Session.Sig.SERVICE) =
struct
  let create_secret session =
    let* token = TokenService.create ~kind:"csrf" ~length:20 () in
    (* Store the ID in the session *)
    (* Storing the token directly could mean it ends up on the client if the cookie
       backend is used for session storage *)
    let* () = SessionService.set session ~key:"csrf" ~value:token.id in
    Lwt.return token
  ;;

  let m () =
    let filter handler req =
      (* Check if session already has a secret (token) *)
      let session =
        match Middleware_session.find_opt req with
        | Some session -> session
        | None ->
          Logs.info (fun m -> m "Have you applied the session middleware?");
          raise (Crypto_failed "No session found")
      in
      let* id = SessionService.get session ~key:"csrf" in
      let* secret =
        match id with
        (* Create a secret if no secret found in session *)
        | None -> create_secret session
        | Some token_id ->
          let* token = TokenService.find_by_id_opt token_id in
          (match token with
          (* Create a secret if invalid token in session *)
          | None -> create_secret session
          (* Return valid secret from session *)
          | Some secret -> Lwt.return secret)
      in
      (* Randomize and scramble secret (XOR with salt) to make a token *)
      (* Do this to mitigate BREACH attacks: http://breachattack.com/#mitigations *)
      let secret_length = String.length secret.value in
      let salt = Core.Random.bytes ~nr:secret_length in
      let secret_value = secret.value |> String.to_seq |> List.of_seq in
      let encrypted =
        match Utils.Encryption.xor salt secret_value with
        | None ->
          Logs.err (fun m -> m "MIDDLEWARE: Failed to encrypt CSRF secret");
          raise @@ Crypto_failed "Failed to encrypt CSRF secret"
        | Some enc -> enc
      in
      let token =
        encrypted
        |> List.append salt
        |> List.to_seq
        |> String.of_seq
        (* Make the token transmittable without encoding problems *)
        |> Base64.encode_string ~alphabet:Base64.uri_safe_alphabet
      in
      let req = set token req in
      (* Don't check for CSRF token in GET requests *)
      (* TODO don't check for HEAD, OPTIONS and TRACE either *)
      if Http.Request.is_get req
      then handler req
      else
        let* value = Http.Request.urlencoded "csrf" req in
        match value with
        (* Give 403 if no token provided *)
        | None -> Http.Response.(create () |> set_status 403) |> Lwt.return
        | Some value ->
          let decoded = Base64.decode ~alphabet:Base64.uri_safe_alphabet value in
          let decoded =
            match decoded with
            | Ok decoded -> decoded
            | Error (`Msg msg) ->
              Logs.err (fun m -> m "MIDDLEWARE: Failed to decode CSRF token. %s" msg);
              raise @@ Crypto_failed ("Failed to decode CSRF token. " ^ msg)
          in
          let salted_cipher = decoded |> String.to_seq |> List.of_seq in
          let decrypted_secret =
            match
              Utils.Encryption.decrypt_with_salt
                ~salted_cipher
                ~salt_length:(List.length salted_cipher / 2)
            with
            | None ->
              Logs.err (fun m -> m "MIDDLEWARE: Failed to decrypt CSRF token");
              raise @@ Crypto_failed "Failed to decrypt CSRF token"
            | Some dec -> dec
          in
          let* provided_secret =
            TokenService.find_opt (decrypted_secret |> List.to_seq |> String.of_seq)
          in
          (match provided_secret with
          | Some ps ->
            if not @@ Token.equal secret ps
            then
              (* Give 403 if provided secret doesn't match session secret *)
              Http.Response.(create () |> set_status 403) |> Lwt.return
            else
              (* Provided secret matches and is valid => Invalidate it so it can't be
                 reused *)
              let* () = TokenService.invalidate ps in
              handler req
          | None ->
            (* Give 403 if provided secret does not exist *)
            Http.Response.(create () |> set_status 403) |> Lwt.return)
    in
    Opium_kernel.Rock.Middleware.create ~name:"csrf" ~filter
  ;;
end
OCaml

Innovation. Community. Security.