Repository: liberapay/liberapay.com

Issue:
CSP breaks redirects after form submission
Chrome and Safari interpret our `form-action 'self'` directive in a weird way, they block the redirect to the elsewhere platform.
CSP breaks redirects after form submission
Chrome and Safari interpret our `form-action 'self'` directive in a weird way, they block the redirect to the elsewhere platform.


Discussion:
This also affects 3DS card payments.
And direct debits.
Spec issue: https://github.com/w3c/webappsec-csp/issues/8
Two possible solutions: #953 (drop `form-action`) or #954 (bypass `form-action`).
I've added a warning to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action about the unclear scope and inconsistent browser implementations of this directive.
At first the client-side redirect solution seemed a bit unclean to me, but after thinking about it I realized that showing an intermediate page before redirecting the user to another website may be a good idea.
#954 did not fix the problem.
This also affects 3DS card payments.
And direct debits.
Spec issue: https://github.com/w3c/webappsec-csp/issues/8
Two possible solutions: #953 (drop `form-action`) or #954 (bypass `form-action`).
I've added a warning to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action about the unclear scope and inconsistent browser implementations of this directive.
At first the client-side redirect solution seemed a bit unclean to me, but after thinking about it I realized that showing an intermediate page before redirecting the user to another website may be a good idea.
#954 did not fix the problem.

Resolving patch:
diff --git a/liberapay/main.py b/liberapay/main.py
--- a/liberapay/main.py
+++ b/liberapay/main.py
@@ -12,6 +12,7 @@
 
 import aspen
 import aspen.http.mapping
+from aspen.request_processor.dispatcher import DispatchResult, DispatchStatus
 from mangopay.utils import Money
 import pando
 from pando import json
@@ -260,6 +261,10 @@ def _redirect(response, url, code=302, trusted_url=True):
     raise Warning('pando.Response.render() already exists')
 def _render(response, path, state, **extra):
     state.update(extra)
+    if 'dispatch_result' not in state:
+        state['dispatch_result'] = DispatchResult(
+            DispatchStatus.okay, path, {}, "Response.render()", {}, False
+        )
     request_processor = state['request_processor']
     output = aspen.resources.get(request_processor, path).render(state)
     fill_response_with_output(output, response, request_processor)
diff --git a/liberapay/utils/state_chain.py b/liberapay/utils/state_chain.py
--- a/liberapay/utils/state_chain.py
+++ b/liberapay/utils/state_chain.py
@@ -173,13 +173,19 @@ def turn_socket_error_into_50X(website, exception, _=lambda a: a, response=None)
     return {'response': response, 'exception': None}
 
 
-def bypass_csp_for_form_redirects(response, state, request=None):
-    if not request:
+def bypass_csp_for_form_redirects(response, state, website, request=None):
+    if request is None:
         return
     # https://github.com/liberapay/liberapay.com/issues/952
     if request.method == 'POST' and response.code == 302:
         target = response.headers[b'Location']
-        if target[:1] in (b'/', b'.'):
+        is_internal = (
+            target[:1] in (b'/', b'.') or
+            target.startswith(b'%s://%s/' % (
+                website.canonical_scheme.encode('ascii'), request.headers[b'Host']
+            ))
+        )
+        if is_internal:
             # Not an external redirect
             return
         try:


Test patch:
diff --git a/tests/py/test_payio.py b/tests/py/test_payio.py
--- a/tests/py/test_payio.py
+++ b/tests/py/test_payio.py
@@ -254,10 +254,9 @@ def test_direct_debit_creation(self, url):
         r = self.client.GET(path, auth_as=self.homer)
         assert b'FRxxxxxxxxxxxxxxxxxxxxx2606' in r.body, r.text
 
-        r = self.client.PxST(path, data, auth_as=self.homer)
-        assert r.code == 302, r.text
-        redir = r.headers[b'Location']
-        assert redir.startswith(b'https://api.sandbox.mangopay.com/')
+        r = self.client.POST(path, data, auth_as=self.homer, raise_immediately=False)
+        assert r.code == 200, r.text
+        assert ';url=https://api.sandbox.mangopay.com/' in r.text
 
         exchange = self.db.one("SELECT * FROM exchanges")
         assert exchange.status == 'pre-mandate'