A tiny web framework for Gleam targeting all JavaScript runtimes
0

Configure Feed

Select the types of activity you want to include in your feed.

:sparkles: add CSP protection middleware

+98 -2
+1
gleam.toml
··· 21 21 gleam_json = ">= 2.3.0 and < 4.0.0" 22 22 marceau = ">= 1.3.0 and < 2.0.0" 23 23 filepath = ">= 1.1.0 and < 2.0.0" 24 + gleam_crypto = ">= 1.5.1 and < 2.0.0" 24 25 25 26 [dev-dependencies] 26 27 gleeunit = ">= 1.0.0 and < 2.0.0"
+2
manifest.toml
··· 3 3 4 4 packages = [ 5 5 { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 6 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 6 7 { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 7 8 { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, 8 9 { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, ··· 13 14 14 15 [requirements] 15 16 filepath = { version = ">= 1.1.0 and < 2.0.0" } 17 + gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" } 16 18 gleam_http = { version = ">= 4.0.0 and < 5.0.0" } 17 19 gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" } 18 20 gleam_json = { version = ">= 2.3.0 and < 4.0.0" }
+62
src/smol.gleam
··· 84 84 //// [read_multipart](#read_multipart "Read and parse a multipart form"), 85 85 //// [handle_head](#handle_head "Handle HEAD requests as GET requests"), 86 86 //// [csrf_known_header_protection](#csrf_known_header_protection "Protect against CSRF attacks"), 87 + //// [content_security_policy_protection](#content_security_policy_protection "Protect against XSS attacks"), 87 88 //// [require_method](#require_method "Middleware to ensure a specific method"), 88 89 //// [require_content_type](#require_content_type "Middleware to ensure a specific content-type header") 89 90 //// ··· 123 124 import gleam/bit_array 124 125 import gleam/bool 125 126 import gleam/bytes_tree.{type BytesTree} 127 + import gleam/crypto 126 128 import gleam/dynamic.{type Dynamic} 127 129 import gleam/dynamic/decode.{type Decoder} 128 130 import gleam/http.{type Method, Get, Head} ··· 609 611 } 610 612 } 611 613 } 614 + } 615 + 616 + /// Protects against cross-site-scripting (XSS) attacks using a nonce-based 617 + /// content-security-policy (CSP). 618 + /// 619 + /// This middleware will provide a unique single use random string (a nonce) to 620 + /// the handler, and set this CSP header on the response returned by the handler. 621 + /// 622 + /// ```txt 623 + /// Content-Security-Policy: 624 + /// script-src 'nonce-{NONCE}' 'strict-dynamic'; 625 + /// object-src 'none'; 626 + /// base-uri 'none'; 627 + /// ``` 628 + /// 629 + /// This header causes the browser to be restricted in these ways: 630 + /// 631 + /// - Any `<script>` tag without a `nonce="..."` property set to the nonce for 632 + /// this request will not be executed. Any scripts created by scripts with 633 + /// the correct `nonce` property will be executed. 634 + /// 635 + /// - Any inline JavaScript event handlers on elements will not be evaluated. 636 + /// e.g. `<span onclick="doSomething();">Click me</span>` will do nothing 637 + /// when clicked. 638 + /// 639 + /// - Any `<object>` or `<embed>` elements will not be executed. 640 + /// 641 + /// - Any use of `<base>` to change the base for relative URLs will be prevented. 642 + /// 643 + /// When using this middleware be sure to add the `nonce="..."` property to all 644 + /// `<script>` elements. 645 + /// 646 + /// ```gleam 647 + /// use csp_nonce <- smol.content_security_policy_protection() 648 + /// ``` 649 + /// ```html 650 + /// <script type="module" nonce="RENDER_YOUR_CSP_NONCE_HERE"> 651 + /// console.log("Hello, Joe!") 652 + /// </script> 653 + /// ``` 654 + /// 655 + /// It is recommended to add this middleware so that it applies to all routes 656 + /// in your application. 657 + /// 658 + /// For more information about CSP see these articles: 659 + /// 660 + /// - <https://web.dev/articles/strict-csp> 661 + /// - <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy> 662 + /// 663 + pub fn content_security_policy_protection( 664 + then handle_request: fn(String) -> Promise(Response), 665 + ) -> Promise(Response) { 666 + let nonce = 667 + crypto.strong_random_bytes(18) |> bit_array.base64_url_encode(False) 668 + let header = 669 + "script-src 'nonce-" 670 + <> nonce 671 + <> "' 'strict-dynamic'; object-src 'none'; base-uri 'none'" 672 + handle_request(nonce) 673 + |> with_extra_headers([#("content-security-policy", header)]) 612 674 } 613 675 614 676 /// A middleware function ensuring that the request has a specific HTTP method.
+33 -2
test/smol_test.gleam
··· 235 235 assert response.status == 400 236 236 237 237 use text <- promise.await(stream_text(response.body)) 238 - assert text == "Bad request: Invalid host" 238 + assert text == "Bad Request" 239 239 240 240 promise.resolve(Nil) 241 241 } ··· 257 257 assert response.status == 400 258 258 259 259 use text <- promise.await(stream_text(response.body)) 260 - assert text == "Bad request: Invalid origin" 260 + assert text == "Bad Request" 261 261 262 262 promise.resolve(Nil) 263 263 } ··· 281 281 }), 282 282 ) 283 283 assert response.status == 200 284 + 285 + promise.resolve(Nil) 286 + } 287 + 288 + pub fn content_security_policy_protection_test() -> Promise(Nil) { 289 + let handler = fn() { 290 + use csp_nonce <- smol.content_security_policy_protection() 291 + smol.send_string(csp_nonce) 292 + } 293 + 294 + use response <- promise.await(handler()) 295 + use nonce <- promise.await(stream_text(response.body)) 296 + 297 + use second_response <- promise.await(handler()) 298 + use second_nonce <- promise.await(stream_text(second_response.body)) 299 + 300 + use third_response <- promise.await(handler()) 301 + use third_nonce <- promise.await(stream_text(third_response.body)) 302 + 303 + // Each time the CSP protection middleware is run it generates a new nonce 304 + assert nonce != second_nonce 305 + assert nonce != third_nonce 306 + 307 + // The CSP header is set 308 + assert_header( 309 + response, 310 + "content-security-policy", 311 + "script-src 'nonce-" 312 + <> nonce 313 + <> "' 'strict-dynamic'; object-src 'none'; base-uri 'none'", 314 + ) 284 315 285 316 promise.resolve(Nil) 286 317 }