diff --git a/docs/web/templates/index.html b/docs/web/templates/index.html
index 5c8a836d..6258c26d 100644
--- a/docs/web/templates/index.html
+++ b/docs/web/templates/index.html
@@ -32,7 +32,7 @@
Tidy Web framework for OCaml and ReasonML
- 1.0.0~alpha2
+ 1.0.0~alpha3
opam install dream
- GitHub
- Edit these docs
diff --git a/example/README.md b/example/README.md
index 30192504..49722708 100644
--- a/example/README.md
+++ b/example/README.md
@@ -99,6 +99,8 @@ if something is missing!
- [**`w-graphql-subscription`**](w-graphql-subscription#files)
— GraphQL subscriptions.
+- [**`w-content-security-policy`**](w-content-security-policy#files)
+ — sandboxes Web pages using `Content-Security-Policy`.
- [**`w-esy`**](w-esy#files) — gives detail on packaging with
[esy](https://esy.sh/), an npm-like package manager.
- [**`w-one-binary`**](w-one-binary#files) — bakes static
diff --git a/example/w-content-security-policy/README.md b/example/w-content-security-policy/README.md
new file mode 100644
index 00000000..5bfee80d
--- /dev/null
+++ b/example/w-content-security-policy/README.md
@@ -0,0 +1,104 @@
+# `w-content-security-policy`
+
+
+
+The [`Content-Security-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
+(CSP) header is used to control where your Web pages can be embedded, and what
+content they can be made to load. This example uses the CSP
+[`frame-ancestors`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors)
+directive to prevent a page from being loaded inside a frame, which can help
+prevent
+[clickjacking](https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html).
+In addition, it tells the browser to send CSP violation reports back to the
+server:
+
+```ocaml
+let home =
+
+
+
+
+
+
+let () =
+ Dream.run
+ @@ Dream.logger
+ @@ Dream.router [
+
+ Dream.get "/" (fun _ ->
+ Dream.html home);
+
+ Dream.get "/nested" (fun _ ->
+ Dream.html
+ ~headers:["Content-Security-Policy",
+ "frame-ancestors 'none'; " ^
+ "report-uri /violation"]
+ "You should not be able to see this inside a frame!");
+
+ Dream.post "/violation" (fun request ->
+ let%lwt report = Dream.body request in
+ Dream.error (fun log -> log "%s" report);
+ Dream.empty `OK);
+
+ ]
+ @@ Dream.not_found
+```
+
+
$ cd example/w-content-security-policy
+$ npm install esy && npx esy
+$ npx esy start
+
+
+
+Visit [http://localhost:8080](http://localhost:8080)
+[[playground](http://dream.as/w-content-security-policy)], and your browser
+should refuse to show `/nested` inside the frame on the home page. In addition,
+the server log will show something like
+
+```
+09.06.21 09:54:35.971 ERROR REQ 3 {
+ "csp-report": {
+ "document-uri": "http://localhost:8080/",
+ "referrer": "",
+ "violated-directive": "frame-ancestors",
+ "effective-directive": "frame-ancestors",
+ "original-policy": "frame-ancestors 'none'; report-uri /violation",
+ "disposition": "enforce",
+ "blocked-uri": "http://localhost:8080/",
+ "status-code": 200,
+ "script-sample": ""
+ }
+}
+```
+
+
+
+You can use CSP to limit which resources can be loaded by the pages you serve,
+forbid execution of JavaScript `eval`, and so on. You may want to apply CSP by
+writing a wrapper around
+[`Dream.html`](https://aantron.github.io/dream/#val-html), or in a middleware.
+Note that static file loaders such as
+[`Dream.from_filesystem`](https://aantron.github.io/dream/#val-from_filesystem)
+can also serve HTML pages, so if you choose not to use a middleware and have
+static HTML pages, be sure to write a custom static loader as well.
+
+Dream does not offer a default CSP, because it will inevitably interfere with
+development, depending on what each Web app is using. Also, it is possible not
+to use CSP at all — CSP is only a defense-in-depth technique. However, it
+is highly recommended to eventually look through the CSP directives as your Web
+app develops. When enabling CSP, also consider
+[`Strict-Transport-Security`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security).
+
+
+
+**See:**
+
+- [`Content-Security-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) on MDN
+- [`Strict-Transport-Security`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) on MDN
+- OWASP [*Content Security Policy Cheat Sheet*](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html)
+- OWASP [*Clickjacking Defense Cheat Sheet*](https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html)
+- OWASP [*HTTP Strict Transport Security Cheat Sheet*](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html)
+
+
+
+[Up to the example index](../#examples)
diff --git a/example/w-content-security-policy/content_security_policy.eml.ml b/example/w-content-security-policy/content_security_policy.eml.ml
new file mode 100644
index 00000000..257442b8
--- /dev/null
+++ b/example/w-content-security-policy/content_security_policy.eml.ml
@@ -0,0 +1,29 @@
+let home =
+
+
+
+
+
+
+let () =
+ Dream.run
+ @@ Dream.logger
+ @@ Dream.router [
+
+ Dream.get "/" (fun _ ->
+ Dream.html home);
+
+ Dream.get "/nested" (fun _ ->
+ Dream.html
+ ~headers:["Content-Security-Policy",
+ "frame-ancestors 'none'; " ^
+ "report-uri /violation"]
+ "You should not be able to see this inside a frame!");
+
+ Dream.post "/violation" (fun request ->
+ let%lwt report = Dream.body request in
+ Dream.error (fun log -> log "%s" report);
+ Dream.empty `OK);
+
+ ]
+ @@ Dream.not_found
diff --git a/example/w-content-security-policy/dune b/example/w-content-security-policy/dune
new file mode 100644
index 00000000..62157b06
--- /dev/null
+++ b/example/w-content-security-policy/dune
@@ -0,0 +1,11 @@
+(executable
+ (name content_security_policy)
+ (libraries dream)
+ (preprocess (pps lwt_ppx)))
+
+(rule
+ (targets content_security_policy.ml)
+ (deps content_security_policy.eml.ml)
+ (action (run dream_eml %{deps} --workspace %{workspace_root})))
+
+(data_only_dirs _esy esy.lock)
diff --git a/example/w-content-security-policy/dune-project b/example/w-content-security-policy/dune-project
new file mode 100644
index 00000000..929c696e
--- /dev/null
+++ b/example/w-content-security-policy/dune-project
@@ -0,0 +1 @@
+(lang dune 2.0)
diff --git a/example/w-content-security-policy/esy.json b/example/w-content-security-policy/esy.json
new file mode 100644
index 00000000..a11a0258
--- /dev/null
+++ b/example/w-content-security-policy/esy.json
@@ -0,0 +1,17 @@
+{
+ "dependencies": {
+ "@opam/dream": "1.0.0~alpha2",
+ "@opam/dune": "^2.0",
+ "ocaml": "4.12.x"
+ },
+ "devDependencies": {
+ "@opam/ocaml-lsp-server": "*",
+ "@opam/ocamlfind-secondary": "*"
+ },
+ "resolutions": {
+ "@opam/conf-libev": "esy-packages/libev:package.json#0b5eb6685b688649045aceac55dc559f6f21b829"
+ },
+ "scripts": {
+ "start": "dune exec --root . ./content_security_policy.exe"
+ }
+}
diff --git a/example/z-playground/server/sync.sh b/example/z-playground/server/sync.sh
index bb317dec..0f3dd1fb 100644
--- a/example/z-playground/server/sync.sh
+++ b/example/z-playground/server/sync.sh
@@ -50,6 +50,7 @@ example i-graphql
example j-stream
example k-websocket
example w-graphql-subscription
+example w-content-security-policy
example w-long-polling
example w-multipart-dump
example w-query
diff --git a/src/dream.mli b/src/dream.mli
index 919a6e17..b00515d2 100644
--- a/src/dream.mli
+++ b/src/dream.mli
@@ -434,7 +434,13 @@ val html :
?headers:(string * string) list ->
string -> response promise
(** Same as {!Dream.respond}, but adds [Content-Type: text/html; charset=utf-8].
- See {!Dream.text_html}. *)
+ See {!Dream.text_html}.
+
+ As your Web app develops, consider adding [Content-Security-Policy] headers,
+ as described in example
+ {{:https://github.com/aantron/dream/tree/master/example/w-content-security-policy#files}
+ [w-content-security-policy]}. These headers are completely optional, but
+ they can provide an extra layer of defense for a mature app. *)
val json :
?status:status ->