thekid / dialog

Dialog photoblog
2 stars 1 forks source link

Ensure CSP is effective against XSS attacks #19

Open thekid opened 1 year ago

thekid commented 1 year ago

Lighthouse says:

A strong Content Security Policy (CSP) significantly reduces the risk of cross-site scripting (XSS) attacks

thekid commented 1 year ago

Based on https://content-security-policy.com/nonce/

  • The nonce must be unique for each HTTP response
  • The nonce should be generated using a cryptographically secure random generator
  • The nonce should have sufficient length, aim for at least 128 bits of entropy (32 hex characters, or about 24 base64 characters).
  • Script tags that have a nonce attribute must not have any untrusted / unescaped variables within them.
  • The characters that can be used in the nonce string are limited to the characters found in base64 encoding.

...here's an implementation:

diff --git a/src/main/handlebars/content.handlebars b/src/main/handlebars/content.handlebars
index 5bb7479..c4380d0 100755
--- a/src/main/handlebars/content.handlebars
+++ b/src/main/handlebars/content.handlebars
@@ -37,7 +37,7 @@ parent: feed
     </section>
   {{/inline}}
   {{#*inline "scripts"}}
-    <script type="module">
+    <script type="module" nonce="{{request.values.nonce}}">
       const markers = {
         style : new ol.style.Style({image: new ol.style.Icon(({src: '/static/marker.png'}))}),
         list  : [],
diff --git a/src/main/handlebars/home.handlebars b/src/main/handlebars/home.handlebars
index 3b5d559..bdc0d58 100755
--- a/src/main/handlebars/home.handlebars
+++ b/src/main/handlebars/home.handlebars
@@ -15,7 +15,7 @@

     <!-- About me -->
     {{#with cover}}
-      <div class="cover" style="background-image: url(/image/{{slug}}/full-{{#with images.0}}{{.}}{{/with}}.webp)">
+      <div class="cover">
         <h1>{{title}}</h1>
       </div>
       <article class="intro">
@@ -55,7 +55,7 @@
     &#187; <a href="/feed">Alle Inhalte im Feed</a>
   {{/inline}}
   {{#*inline "scripts"}}
-    <script type="module">
+    <script type="module" nonce="{{request.values.nonce}}">
       {{&use 'suggestions'}}
       suggestions(document.querySelector('#search'), 'Volltextsuche nach "%s"');
     </script>
diff --git a/src/main/handlebars/journey.handlebars b/src/main/handlebars/journey.handlebars
index 434d93b..d21b12c 100755
--- a/src/main/handlebars/journey.handlebars
+++ b/src/main/handlebars/journey.handlebars
@@ -77,7 +77,7 @@ parent: feed
     {{/with}}
   {{/inline}}
   {{#*inline "scripts"}}
-    <script type="module">
+    <script type="module" nonce="{{request.values.nonce}}">
       {{&use 'mapping'}}

       {{#each itinerary}}
diff --git a/src/main/handlebars/journeys.handlebars b/src/main/handlebars/journeys.handlebars
index c2bbae3..2693942 100755
--- a/src/main/handlebars/journeys.handlebars
+++ b/src/main/handlebars/journeys.handlebars
@@ -28,7 +28,7 @@
     </div>
   {{/inline}}
   {{#*inline "scripts"}}
-    <script type="module">
+    <script type="module" nonce="{{request.values.nonce}}">
       {{&use 'mapping'}}

       {{#each journeys}}
diff --git a/src/main/handlebars/layout.handlebars b/src/main/handlebars/layout.handlebars
index 5d496e6..beedd98 100755
--- a/src/main/handlebars/layout.handlebars
+++ b/src/main/handlebars/layout.handlebars
@@ -2,13 +2,14 @@
 <html lang="de" prefix="og: http://ogp.me/ns#">
 <head>
   <meta charset="utf-8">
+  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-{{request.values.nonce}}'; style-src 'self' 'nonce-{{request.values.nonce}}'; img-src 'self' https://tile.openstreetmap.org">
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
   <meta name="description" content="Fotoblog von Timm Friebe">
   <meta name="twitter:card" content="summary">
   <meta name="twitter:site" content="@timmfriebe">
   {{> meta}}
   <link rel="stylesheet" type="text/css" href="/assets/{{asset 'vendor.css'}}">
-  <style type="text/css">
+  <style type="text/css" nonce="{{request.values.nonce}}">
     /* CSS reset, see https://piccalil.li/blog/a-modern-css-reset/ */
     *, *::before, *::after {
       box-sizing: border-box;
@@ -109,6 +110,7 @@

     .cover {
       margin: -1rem -1rem 1rem -1rem;
+      background-image: url(/image/{{cover.slug}}/full-{{cover.images.0.name}}.webp);
       background-size: cover;
       background-position: center;
       height: 50vh;
diff --git a/src/main/handlebars/search.handlebars b/src/main/handlebars/search.handlebars
index 17f7ca1..deec835 100755
--- a/src/main/handlebars/search.handlebars
+++ b/src/main/handlebars/search.handlebars
@@ -54,7 +54,7 @@ parent: feed
     </div>
   {{/inline}}
   {{#*inline "scripts"}}
-    <script type="module">
+    <script type="module" nonce="{{request.values.nonce}}">
       {{&use 'suggestions'}}
       suggestions(document.querySelector('#search'), 'Volltextsuche nach "%s"');
     </script>
diff --git a/src/main/php/de/thekid/dialog/App.php b/src/main/php/de/thekid/dialog/App.php
index 8afcb3f..1541a70 100755
--- a/src/main/php/de/thekid/dialog/App.php
+++ b/src/main/php/de/thekid/dialog/App.php
@@ -28,6 +28,9 @@ class App extends Application {
       $this->install(new BehindProxy([$service => '/']));
     }

+    // Generate nonces for every request
+    $this->install(new Nonce());
+
     // Cache static content for one week, immutable fingerprinted assets for one year
     $manifest= new AssetsManifest($this->environment->path('src/main/webapp/assets/manifest.json'));
     $static= ['Cache-Control' => 'max-age=604800'];

The Nonce filter is implemented as follows:

<?php namespace de\thekid\dialog;

use util\Random;
use web\Filter;

/**
 * Generates nonce for every request to be used in CSP
 *
 * @see  https://content-security-policy.com/nonce/
 */
class Nonce implements Filter {
  private $random= new Random();

  /**
   * Filtering implementation
   *
   * @param  web.Request $req
   * @param  web.Response $res
   * @param  web.filters.Invocation $invocation
   * @return var
   */
  public function filter($req, $res, $invocation) {
    return $invocation->proceed($req->pass('nonce', bin2hex($this->random->bytes(16))), $res);
  }
}
thekid commented 1 year ago

We should start with default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self' to prevent any other sources, e.g. iframes or objects from loading.

thekid commented 1 year ago

Cool GOTO conference talk at https://www.youtube.com/watch?v=mr230uotw-Y

thekid commented 1 year ago

Small improvement released in https://github.com/thekid/dialog/releases/tag/v1.6.1, now sets X-Frame-Options and Referrer-Policy headers.