Repository: liberapay/liberapay.com

Issue:
CSP blocks inline styles of notifications
In #924 I replaced all inline styles except those of notifications. The problem is that we don't only send those inline styles in HTML emails, they also end up in the user's notifications page.


Discussion:
The quick fix is to allow inline styles for the notifications page.

The proper fix would be to stop using inline styles in notification templates, and use a library to compile to inline styling when sending emails.
The CSP also blocks the "Donation button" widget's fill from loading (https://liberapay.com/%username/widgets/). 

```
Content Security Policy: 
The page’s settings blocked the loading of a resource at self (“default-src”).
```

```html
<a class="liberapay-btn" href="https://liberapay.com/EdOverflow/donate">
    <svg viewBox="0 0 80 80" height="16" width="16">
   <g transform="translate(-78.37-208.06)" fill="#1a171b">
   ...
   <span>Donate</span>
</a>
```

![image](https://user-images.githubusercontent.com/18099289/35289487-c75c1a66-0067-11e8-96fd-3283d560c882.png)


Resolving patch:
diff --git a/liberapay/security/__init__.py b/liberapay/security/__init__.py
--- a/liberapay/security/__init__.py
+++ b/liberapay/security/__init__.py
@@ -13,17 +13,7 @@ def set_default_security_headers(website, response, request=None):
 
     # CSP is a client-side protection against code injection (XSS)
     # https://scotthelme.co.uk/content-security-policy-an-introduction/
-    csp = (
-        b"default-src 'self' %(main_domain)s;"
-        b"connect-src 'self' *.liberapay.org *.mangopay.com *.payline.com;"
-        b"form-action 'self';"
-        b"img-src * blob: data:;"
-        b"object-src 'none';"
-    ) % {b'main_domain': website.canonical_host.encode('ascii')}
-    csp += website.env.csp_extra.encode()
-    if website.canonical_scheme == 'https':
-        csp += b"upgrade-insecure-requests;"
-    response.headers[b'content-security-policy'] = csp
+    response.headers[b'content-security-policy'] = website.csp
 
     # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
     response.headers[b'X-XSS-Protection'] = b'1; mode=block'
diff --git a/liberapay/wireup.py b/liberapay/wireup.py
--- a/liberapay/wireup.py
+++ b/liberapay/wireup.py
@@ -67,6 +67,46 @@ def canonical(env):
     return locals()
 
 
+class CSP(bytes):
+
+    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
+    based_on_default_src = set(b'''
+        child-src connect-src font-src frame-src img-src manifest-src
+        media-src object-src script-src style-src worker-src
+    '''.split())
+
+    def __new__(cls, x):
+        if isinstance(x, dict):
+            self = bytes.__new__(cls, b';'.join(b' '.join(t) for t in x.items()) + b';')
+            self.directives = dict(x)
+        else:
+            self = bytes.__new__(cls, x)
+            self.directives = dict(d.split(b' ', 1) for d in self.split(b';') if d)
+        return self
+
+    def allow(self, directive, value):
+        d = dict(self.directives)
+        old_value = d.get(directive)
+        if old_value is None and directive in self.based_on_default_src:
+            old_value = d.get(b'default-src')
+        d[directive] = b'%s %s' % (old_value, value) if old_value else value
+        return CSP(d)
+
+
+def csp(canonical_host, canonical_scheme, env):
+    csp = (
+        b"default-src 'self' %(main_domain)s;"
+        b"connect-src 'self' *.liberapay.org *.mangopay.com *.payline.com;"
+        b"form-action 'self';"
+        b"img-src * blob: data:;"
+        b"object-src 'none';"
+    ) % {b'main_domain': canonical_host.encode('ascii')}
+    csp += env.csp_extra.encode()
+    if canonical_scheme == 'https':
+        csp += b"upgrade-insecure-requests;"
+    return {'csp': CSP(csp)}
+
+
 class NoDB(object):
 
     def __getattr__(self, attr):
@@ -709,6 +749,7 @@ def currency_exchange_rates(db):
     make_sentry_teller,
     database,
     canonical,
+    csp,
     app_conf,
     mail,
     billing,


Test patch: