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

Ensure CSP is effective against XSS attacks #19

Open
thekid opened this issue Oct 10, 2022 · 4 comments
Open

Ensure CSP is effective against XSS attacks #19

thekid opened this issue Oct 10, 2022 · 4 comments
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed

Comments

@thekid
Copy link
Owner

thekid commented Oct 10, 2022

Lighthouse says:

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

@thekid thekid added enhancement New feature or request help wanted Extra attention is needed good first issue Good for newcomers labels Oct 10, 2022
@thekid
Copy link
Owner Author

thekid commented Oct 23, 2022

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
Copy link
Owner Author

thekid commented Oct 23, 2022

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
Copy link
Owner Author

thekid commented Oct 23, 2022

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

thekid added a commit that referenced this issue Oct 29, 2022
Adds default security headers, see issue #19 and xp-forge/frontend#31
@thekid
Copy link
Owner Author

thekid commented Oct 29, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant