Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add csrf_tag helper #201

Merged
merged 3 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions example/d-form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ let show_form ?message request =
<p>You entered: <b><%s message %>!</b></p>
% end;

<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="message" autofocus>
</form>

Expand Down Expand Up @@ -53,13 +54,13 @@ Try it in the [playground](http://dream.as/d-form).

<br>

We didn't write a literal `<form>` tag in the template. Instead, we used
[`Dream.form_tag`](https://aantron.github.io/dream/#val-form_tag) to generate
the tag. [`Dream.form_tag`](https://aantron.github.io/dream/#val-form_tag) also
snuck in a hidden `<input>` field containing a CSRF token:
We wrote a literal `<form>` tag in the template, and injected a field containing
a CSRF token into it using the
[`Dream.csrf_tag`](https://aantron.github.io/dream/#val-csrf_tag) helper to
generate the `<input>` tag.

```html
<form method="POST" action="/">
<form method="post" action="/">
<input name="dream.csrf" type="hidden" value="j8vjZ6...">

<!-- The rest we actually wrote ourselves in the template! -->
Expand Down
3 changes: 2 additions & 1 deletion example/d-form/form.eml.ml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ let show_form ?message request =
<p>You entered: <b><%s message %>!</b></p>
% end;

<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="message" autofocus>
</form>

Expand Down
13 changes: 7 additions & 6 deletions example/g-upload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ sizes:
let home request =
<html>
<body>
<%s! Dream.form_tag ~action:"/" ~enctype:`Multipart_form_data request %>
<form action="/" method="post" enctype="multipart/form-data">
<%s! Dream.csrf_tag request %>
<input name="files" type="file" multiple>
<button>Submit!</button>
</form>
Expand Down Expand Up @@ -85,13 +86,13 @@ streaming file uploads.
[`Dream.multipart`](https://aantron.github.io/dream/#val-multipart) behaves just
like [`Dream.form`](https://aantron.github.io/dream/#val-form) when it comes to
[CSRF protection](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html).
See example [**`d-form`**](../d-form#files). We still use
[`Dream.form_tag`](https://aantron.github.io/dream/#val-form_tag) to generate
the form in the template. The only difference is that we now pass it
``~enctype:`Multipart_form_data`` to make its output look like this:
See example [**`d-form`**](../d-form#files). We use
[`Dream.csrf_tag`](https://aantron.github.io/dream/#val-csrf_tag) to generate
the CSRF token in the template, and pass the `enctype="multipart/form-data"`
attribute as needed for forms to upload files:

```html
<form method="POST" action="/" enctype="multipart/form-data">
<form method="post" action="/" enctype="multipart/form-data">
<input name="dream.csrf" type="hidden" value="...">

<!-- Our fields -->
Expand Down
3 changes: 2 additions & 1 deletion example/g-upload/upload.eml.ml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
let home request =
<html>
<body>
<%s! Dream.form_tag ~action:"/" ~enctype:`Multipart_form_data request %>
<form action="/" method="post" enctype="multipart/form-data">
<%s! Dream.csrf_tag request %>
<input name="files" type="file" multiple>
<button>Submit!</button>
</form>
Expand Down
3 changes: 2 additions & 1 deletion example/h-sql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ let render comments request =
% comments |> List.iter (fun (_id, comment) ->
<p><%s comment %></p><% ); %>

<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="text" autofocus>
</form>

Expand Down
3 changes: 2 additions & 1 deletion example/h-sql/sql.eml.ml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ let render comments request =
% comments |> List.iter (fun (_id, comment) ->
<p><%s comment %></p><% ); %>

<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="text" autofocus>
</form>

Expand Down
3 changes: 2 additions & 1 deletion example/w-flash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ absolutely primitive form with just one field:
let form request =
<html>
<body>
<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="text" autofocus>
</form>
</body>
Expand Down
3 changes: 2 additions & 1 deletion example/w-flash/flash.eml.ml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
let form request =
<html>
<body>
<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="text" autofocus>
</form>
</body>
Expand Down
3 changes: 2 additions & 1 deletion example/w-multipart-dump/multipart_dump.eml.ml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
let home request =
<html>
<body>
<%s! Dream.form_tag ~action:"/" ~enctype:`Multipart_form_data request %>
<form action="/" method="post" enctype="multipart/form-data">
<%s! Dream.csrf_tag request %>
<input name="text"><br>
<input name="files" type="file" multiple><br>
<button>Submit!</button>
Expand Down
3 changes: 2 additions & 1 deletion example/w-postgres/postgres.eml.ml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ let render comments request =
% comments |> List.iter (fun (_id, comment) ->
<p><%s comment %></p><% ); %>

<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="text" autofocus>
</form>

Expand Down
5 changes: 3 additions & 2 deletions example/w-upload-stream/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ the total size of each uploaded file:
let home request =
<html>
<body>
<%s! Dream.form_tag ~action:"/" ~enctype:`Multipart_form_data request %>
<form action="/" enctype="multipart/form-data" method="post">
<%s! Dream.csrf_tag request %>
<input name="files" type="file" multiple>
<button>Submit!</button>
</form>
Expand Down Expand Up @@ -70,7 +71,7 @@ Try it in the [playground](http://dream.as/w-upload-stream).

The report page shows one file without a name ("None"). This is, in fact, the
CSRF token generated by
[`Dream.form_tag`](https://aantron.github.io/dream/#val-form_tag) inside the
[`Dream.csrf_tag`](https://aantron.github.io/dream/#val-csrf_tag) inside the
template. To keep the example simple, we didn't check the CSRF token, nor filter
out the `dream.csrf` field that it appears in. If you'd like to do so in your
code, see
Expand Down
3 changes: 2 additions & 1 deletion example/w-upload-stream/upload_stream.eml.ml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
let home request =
<html>
<body>
<%s! Dream.form_tag ~action:"/" ~enctype:`Multipart_form_data request %>
<form action="/" enctype="multipart/form-data" method="post">
<%s! Dream.csrf_tag request %>
<input name="files" type="file" multiple>
<button>Submit!</button>
</form>
Expand Down
1 change: 1 addition & 0 deletions src/dream.ml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ let verify_csrf_token = Csrf.verify_csrf_token ~now
let form_tag ?method_ ?target ?enctype ?csrf_token ~action request =
Tag.form_tag ~now ?method_ ?target ?enctype ?csrf_token ~action request

let csrf_tag = Tag.csrf_tag ~now


(* Middleware *)
Expand Down
54 changes: 39 additions & 15 deletions src/dream.mli
Original file line number Diff line number Diff line change
Expand Up @@ -910,12 +910,13 @@ val origin_referrer_check : middleware

(** {1 Forms}

{!Dream.form_tag} and {!Dream.val-form} round-trip secure forms.
{!Dream.form_tag} is used inside a template to generate a form header with a
CSRF token:
{!Dream.csrf_tag} and {!Dream.val-form} round-trip secure forms.
{!Dream.csrf_tag} is used inside a form template to generate a hidden field
with a CSRF token:

{[
<%s! Dream.form_tag ~action:"/" request %>
<form action="/" method="post">
<%s! Dream.csrf_tag request %>
<input name="my.field">
</form>
]}
Expand Down Expand Up @@ -956,13 +957,13 @@ type 'a form_result = [

val form : ?csrf:bool -> request -> (string * string) list form_result promise
(** Parses the request body as a form. Performs CSRF checks. Use
{!Dream.form_tag} in a template to transparently generate forms that will
pass these checks. See {!section-templates} and example
{!Dream.csrf_tag} in a form template to transparently generate forms that
will pass these checks. See {!section-templates} and example
{{:https://github.com/aantron/dream/tree/master/example/d-form#readme}
[d-form]}.

- [Content-Type:] must be [application/x-www-form-urlencoded].
- The form must have a field named [dream.csrf]. {!Dream.form_tag} adds such
- The form must have a field named [dream.csrf]. {!Dream.csrf_tag} adds such
a field.
- {!Dream.form} calls {!Dream.verify_csrf_token} to check the token in
[dream.csrf].
Expand Down Expand Up @@ -1047,15 +1048,14 @@ type multipart_form =

val multipart : ?csrf:bool -> request -> multipart_form form_result promise
(** Like {!Dream.form}, but also reads files, and [Content-Type:] must be
[multipart/form-data]. The [<form>] tag and CSRF token can be generated in a
template with
[multipart/form-data]. The CSRF token can be generated in a template with

{[
<%s! Dream.form_tag ~action:"/"
~enctype:`Multipart_form_data request %>
<form action="/" method="post" enctype="multipart/form-data">
<%s! Dream.csrf_tag request %>
]}

See {!Dream.form_tag}, section {!section-templates}, and example
See section {!section-templates}, and example
{{:https://github.com/aantron/dream/tree/master/example/g-upload#files}
[g-upload]}.

Expand Down Expand Up @@ -1090,7 +1090,7 @@ val upload : request -> part option promise
{!Dream.upload} does not verify a CSRF token. There are several ways to add
CSRF protection for an upload stream, including:

- Generate the form with {!Dream.form_tag}. Check for
- Generate a CSRF token with {!Dream.csrf_tag}. Check for
[`Field ("dream.csrf", token)] during upload and call
{!Dream.verify_csrf_token}.
- Use {{:https://developer.mozilla.org/en-US/docs/Web/API/FormData}
Expand All @@ -1104,8 +1104,9 @@ val upload_part : request -> string option promise

It's usually not necessary to handle CSRF tokens directly.

- Form tag generator {!Dream.form_tag} generates and inserts a CSRF token
that {!Dream.val-form} and {!Dream.val-multipart} transparently verify.
- CSRF token field generator {!Dream.csrf_tag} generates and inserts a CSRF
token that {!Dream.val-form} and {!Dream.val-multipart} transparently
verify.
- AJAX can be protected from CSRF by {!Dream.origin_referrer_check}.

CSRF functions are exposed for creating custom schemes, and for
Expand Down Expand Up @@ -1228,6 +1229,29 @@ let render message =
unquoted attribute values, CSS in [<style>] tags, or literal JavaScript in
[<script>] tags. *)

val csrf_tag : request -> string
(** Generates an [<input>] tag with a CSRF token, suitable for use with
{!Dream.val-form} and {!Dream.val-multipart}. For example, in a template,

{[
<form method="post" action="/">
<%s! Dream.csrf_tag request %>
<input name="my.field">
</form>
]}

expands to

{[
<form method="post" action="/">
<input name="dream.csrf" type="hidden" value="a-token">
<input name="my.field">
</form>
]}

It is recommended to put the CSRF tag immediately after the starting
[<form>] tag, to prevent certain kinds of DOM manipulation-based attacks. *)

val form_tag :
?method_:[< method_ ] ->
?target:string ->
Expand Down
15 changes: 8 additions & 7 deletions src/mirage/mirage.ml
Original file line number Diff line number Diff line change
Expand Up @@ -122,28 +122,28 @@ module Make (Pclock : Mirage_clock.PCLOCK) (Time : Mirage_time.S) (Stack : Mirag
include Dream__middleware.Log
include Dream__middleware.Log.Make (Pclock)
include Dream__middleware.Echo

let default_log =
Dream__middleware.Log.sub_log (Logs.Src.name Logs.default)

let error = default_log.error
let warning = default_log.warning
let info = default_log.info
let debug = default_log.debug

include Dream__middleware.Router

include Dream__middleware.Session
include Dream__middleware.Session.Make (Pclock)

include Dream__middleware.Origin_referrer_check
include Dream__middleware.Form
include Dream__middleware.Upload
include Dream__middleware.Csrf

let content_length =
Dream__middleware.Content_length.content_length

include Dream__middleware.Lowercase_headers
include Dream__middleware.Catch
include Dream__middleware.Request_id
Expand All @@ -168,8 +168,9 @@ module Make (Pclock : Mirage_clock.PCLOCK) (Time : Mirage_time.S) (Stack : Mirag
let multipart = multipart ~now
let csrf_token = csrf_token ~now
let verify_csrf_token = verify_csrf_token ~now
let csrf_tag = csrf_tag ~now
let form_tag = form_tag ~now

include Dream__pure.Formats

include Paf_mirage.Make (Time) (Stack)
Expand Down
2 changes: 2 additions & 0 deletions src/mirage/mirage.mli
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ module Make
val form : ?csrf:bool -> request -> (string * string) list form_result Lwt.t
val multipart : ?csrf:bool -> request -> multipart_form form_result Lwt.t

val csrf_tag : request -> string

val form_tag :
?method_:method_ ->
?target:string ->
Expand Down
6 changes: 4 additions & 2 deletions src/server/tag.eml.ml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ end
module Method = Dream_pure.Method


let csrf_tag ~now request =
let token = Csrf.csrf_token ~now request in
<input name="<%s! Csrf.field_name %>" type="hidden" value="<%s! token %>">

(* TODO Include the path prefix. *)
let form_tag
Expand Down Expand Up @@ -45,6 +48,5 @@ let form_tag
method="<%s! method_ %>"
action="<%s action %>"<%s! target %><%s! enctype %>>
% if csrf_token then begin
% let token = Csrf.csrf_token ~now request in
<input name="<%s! Csrf.field_name %>" type="hidden" value="<%s! token %>">
<%s! csrf_tag ~now request %>
% end;