Open thekid opened 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 @@
» <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);
}
}
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.
Cool GOTO conference talk at https://www.youtube.com/watch?v=mr230uotw-Y
Small improvement released in https://github.com/thekid/dialog/releases/tag/v1.6.1, now sets X-Frame-Options
and Referrer-Policy
headers.
Lighthouse says: