123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144moduleCore=Sihl_coremoduleUtils=Sihl_utilsmoduleHttp=Sihl_httpmoduleToken=Sihl_tokenmoduleSession=Sihl_sessionopenLwt.Syntaxletlog_src=Logs.Src.create~doc:"CSRF Middleware""sihl.middleware.csrf"moduleLogs=(valLogs.src_loglog_src:Logs.LOG)letkey:stringOpium_kernel.Hmap.key=Opium_kernel.Hmap.Key.create("csrf token",Sexplib.Std.sexp_of_string);;exceptionCrypto_failedofstring(* Can be used to fetch token in view for forms *)letfindreq=Opium_kernel.Hmap.find_exnkey(Opium_kernel.Request.envreq)letfind_optreq=trySome(findreq)with|_->None;;letsettokenreq=letenv=Opium_kernel.Request.envreqinletenv=Opium_kernel.Hmap.addkeytokenenvin{reqwithenv};;(* 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
*)moduleMake(TokenService:Token.Sig.SERVICE)(SessionService:Session.Sig.SERVICE)=structletcreate_secretsession=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.setsession~key:"csrf"~value:token.idinLwt.returntoken;;letm()=letfilterhandlerreq=(* Check if session already has a secret (token) *)letsession=matchMiddleware_session.find_optreqwith|Somesession->session|None->Logs.info(funm->m"Have you applied the session middleware?");raise(Crypto_failed"No session found")inlet*id=SessionService.getsession~key:"csrf"inlet*secret=matchidwith(* Create a secret if no secret found in session *)|None->create_secretsession|Sometoken_id->let*token=TokenService.find_by_id_opttoken_idin(matchtokenwith(* Create a secret if invalid token in session *)|None->create_secretsession(* Return valid secret from session *)|Somesecret->Lwt.returnsecret)in(* Randomize and scramble secret (XOR with salt) to make a token *)(* Do this to mitigate BREACH attacks: http://breachattack.com/#mitigations *)letsecret_length=String.lengthsecret.valueinletsalt=Core.Random.bytes~nr:secret_lengthinletsecret_value=secret.value|>String.to_seq|>List.of_seqinletencrypted=matchUtils.Encryption.xorsaltsecret_valuewith|None->Logs.err(funm->m"MIDDLEWARE: Failed to encrypt CSRF secret");raise@@Crypto_failed"Failed to encrypt CSRF secret"|Someenc->encinlettoken=encrypted|>List.appendsalt|>List.to_seq|>String.of_seq(* Make the token transmittable without encoding problems *)|>Base64.encode_string~alphabet:Base64.uri_safe_alphabetinletreq=settokenreqin(* Don't check for CSRF token in GET requests *)(* TODO don't check for HEAD, OPTIONS and TRACE either *)ifHttp.Request.is_getreqthenhandlerreqelselet*value=Http.Request.urlencoded"csrf"reqinmatchvaluewith(* Give 403 if no token provided *)|None->Http.Response.(create()|>set_status403)|>Lwt.return|Somevalue->letdecoded=Base64.decode~alphabet:Base64.uri_safe_alphabetvalueinletdecoded=matchdecodedwith|Okdecoded->decoded|Error(`Msgmsg)->Logs.err(funm->m"MIDDLEWARE: Failed to decode CSRF token. %s"msg);raise@@Crypto_failed("Failed to decode CSRF token. "^msg)inletsalted_cipher=decoded|>String.to_seq|>List.of_seqinletdecrypted_secret=matchUtils.Encryption.decrypt_with_salt~salted_cipher~salt_length:(List.lengthsalted_cipher/2)with|None->Logs.err(funm->m"MIDDLEWARE: Failed to decrypt CSRF token");raise@@Crypto_failed"Failed to decrypt CSRF token"|Somedec->decinlet*provided_secret=TokenService.find_opt(decrypted_secret|>List.to_seq|>String.of_seq)in(matchprovided_secretwith|Someps->ifnot@@Token.equalsecretpsthen(* Give 403 if provided secret doesn't match session secret *)Http.Response.(create()|>set_status403)|>Lwt.returnelse(* Provided secret matches and is valid => Invalidate it so it can't be
reused *)let*()=TokenService.invalidatepsinhandlerreq|None->(* Give 403 if provided secret does not exist *)Http.Response.(create()|>set_status403)|>Lwt.return)inOpium_kernel.Rock.Middleware.create~name:"csrf"~filter;;end