CSRF checking in Webtest

208 views
Skip to first unread message

Mike Orr

unread,
Sep 25, 2017, 8:19:22 PM9/25/17
to pylons-...@googlegroups.com
I have a Pyramid application which recently upgraded its login view to
do CSRF checking. I set 'require_csrf=True' in the view config. I
didn't set any of the configurator options for csrf so it defaults to
session storage, which is 'pyramid_redis_sessions'.

The application works fine but when I ran py.test to see if any tests
were out of date, all the functional tests broke with this error:

AppError: Bad Response: 400 Bad CSRF Origin (not 200 OK or 3xx
redirect for https://localhost:90/login)
400 Bad CSRF Origin
Access is denied. The server can not verify that the origin or
referrer of your request matches the current site. Either your browser
supplied the wrong Origin or Referrer or it did not supply one at all.
Referer checking failed - https://localhost:80/login does not match
any trusted origins.

My test fixture, common login function, and test look like this:

# Fixture
app = pyramid.paster.get_app(ini_file)
extra_environ = {"wsgi.url_scheme": "https"}
apptest = webtest.TestApp(app, extra_environ=extr_environ)
# Test
login(apptest, USERNAME)
# Login function
apptest.reset() # Clear any existing cookies.
res = apptest.get("/login", status=200)
form = res.forms["login"]
form["username"] = USERNAME
form["password"] = PASSWORD
res = form.submit()
assert b"Invalid username or password." not in res

This seems to be a different error from a token mismatch or missing
token. There is a session with a key "_csrft_" so it seems to have a
token that should work. Looking at the source code for
"pyramid/csrf.py" in the "check_csrf_origin()" function, it talks
about an "Origin" header and trusted origins and a config setting to
add trusted origins. I added a setting:

pyramid.csrf_trusted_origins = localhost:80

and that made the test succeed. But is that the most robust approach?
And why do I have to do this? There is no 'localhost' server or port
80 running -- it's a figment of WebTest's imagination. So it could
just as easily be "foo:999999". But whatever WebTest's default is,
shouldn't it be the same across tests and shouldn't that be enough for
the CSRF checker? What is the trusted origin it's expecting?

Perhaps it's a mismatch between the https scheme and port 80. I think
I set HTTPS because the cookies are HTTPS only and they wouldn't go
through otherwise. But how do you set the port in the WSGI
environment? And again, why should I have to?

Another option would be to disable CSRF checking during tests. That
would require defining a setting and passing it through multiple
layers to the view registration, and again is less than ideal because
I'd rather have CSRF checking work rather than be bypassed in tests.

So what's the best way forward?

--
Mike Orr <slugg...@gmail.com>

Michael Merickel

unread,
Sep 25, 2017, 8:48:13 PM9/25/17
to Pylons
> So what's the best way forward?

I think you covered your options pretty well.

1) Set wsgi.url_scheme to "http" as origin checks are only done on https.
2) Set the pyramid.csrf_trusted_origins as you are doing now.
3) Disable csrf checking for your tests.

I think it's just a helpful reminder that you would be wise to think about the origin header more these days as it's required by CORS requests and, of course, cross origin requests are the attack vector CSRF is helping to protect.

Mike Orr

unread,
Sep 26, 2017, 12:00:51 AM9/26/17
to pylons-...@googlegroups.com
It sounds like it needs documentation then. What is the Origin header
and shouldn't Pyramid/WebOb set it automatically if it's becoming more
important?

#1 and #3 would make the test environment different from the real
environment. #2 raises the question of what is WebTest's Origin
header, what should it be, why are they different, and does something
need to be changed in the library?

--
Mike Orr <slugg...@gmail.com>

Mike Orr

unread,
Sep 26, 2017, 1:40:52 AM9/26/17
to pylons-...@googlegroups.com
I guess the solution is #1, to roll back the HTTPS, because there is
no HTTPS because there's no network server. That in turn will require
a configuration that doesn't make the cookies HTTPS-only.

>
> --
> Mike Orr <slugg...@gmail.com>



--
Mike Orr <slugg...@gmail.com>

Mike Orr

unread,
Sep 26, 2017, 1:52:26 AM9/26/17
to pylons-...@googlegroups.com
(The browser got in a mood and did "Send" too quickly.)

I still feel like there's a missing piece, something that needs to be
documented so that others don't fall into this same trap. The only
reason I set HTTPS-only and CSRF is our IT department asked us to do
this wherever feasible, and since it didn't make much difference
either way I went along with it. So presumably other people in other
organizations will be doing the same thing, and have the same thing
happen in their tests.


--
Mike Orr <slugg...@gmail.com>

Michael Merickel

unread,
Sep 26, 2017, 11:15:19 AM9/26/17
to Pylons
> What is the Origin header and shouldn't Pyramid/WebOb set it automatically if it's becoming more important?

1) You can read the RFC about it or anything on google.
2) The Origin header is set by the client, not the server.

If the origin matches the current domain (usually set by the host header) then the request is trusted by default and you do not need to modify anything. You have your app setup in such a way that your requests *look* like they are originating from another server instead of the domain hosting the content. Just configure your webtest requests such that the origin and host match and you'll be fine.

I still feel like there's a missing piece, something that needs to be documented so that others don't fall into this same trap.

If you would like to contribute some documentation on this once you figure it out I'm more than happy to review. It probably belongs in the testing chapter about how to use webtest. If you think webtest should set the origin/host the same by default then perhaps you could open an issue over there.

- Michael




--
Mike Orr <slugg...@gmail.com>

--
You received this message because you are subscribed to the Google Groups "pylons-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pylons-discuss+unsubscribe@googlegroups.com.
To post to this group, send email to pylons-discuss@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pylons-discuss/CAH9f%3Duo%2BAYpm6-oS-vPLFg0Ek3gz1G2JDHcW2m0MmdH_X8zp4g%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Mike Orr

unread,
Oct 11, 2017, 6:51:20 PM10/11/17
to pylons-...@googlegroups.com
On Tue, Sep 26, 2017 at 8:14 AM, Michael Merickel <mmer...@gmail.com> wrote:
>> What is the Origin header and shouldn't Pyramid/WebOb set it automatically
>> if it's becoming more important?
>
> 1) You can read the RFC about it or anything on google.
> 2) The Origin header is set by the client, not the server.
>
> If the origin matches the current domain (usually set by the host header)
> then the request is trusted by default and you do not need to modify
> anything. You have your app setup in such a way that your requests *look*
> like they are originating from another server instead of the domain hosting
> the content. Just configure your webtest requests such that the origin and
> host match and you'll be fine.
>
>> I still feel like there's a missing piece, something that needs to be
>> documented so that others don't fall into this same trap.
>
> If you would like to contribute some documentation on this once you figure
> it out I'm more than happy to review. It probably belongs in the testing
> chapter about how to use webtest. If you think webtest should set the
> origin/host the same by default then perhaps you could open an issue over
> there.

OK, I've got something that works; I'll make a cookbook page for
testing with HTTPS-only cookies. I'll try to explain it precisely now
although it's a long journey through webtest.app, webob.request,
pyramid.csrf, and session cookies. To recap the situation:

1. The login form is CSRF-protected. (IT requirement.)

2. The app uses SessionAuthenticationPolicy and
pyramid_redis_sessions. Cookies are HTTPS-only (IT requirement.), so
setting 'redis.sessions.cookie_secure = true'. This tells the client
to send the cookie only with HTTPS requests (not HTTP).

3. All this is working fine in production.

4. The functional test logs in via a form. It uses WebTest. There is
no web server, just WebTest calling the WSGI app.

5. The test ran fine before implementing HTTPS cookies and CSRF protection.

6. HTTPS cookies broke the test, because WebTest emulates an HTTP
environment by default, so it won't transmit the cookie that indicates
the session containing the authenticated status. The login submit
fails.

7.I made the request "HTTPS" with an environ var:

``extra_environ = {"wsgi.url_scheme": "https"}``.
``apptest = webtest.testApp(app, extra_environ=extra_environ}``.
That fixed #6.

8. I implemented CSRF checking, and now the login failed because the
"origin" (the client's domain) was different was different from what
the "server" expected.

9. Troubleshooting and this list revealed two workarounds: turn off
HTTPS-only cookies and revert to HTTP, or set the de facto host as a
trusted host. (Setting 'pyramid.csrf_trusted_origins = localhost:80'.)

10. However, I didn't want to make my test configuration so different
from the real one and lose some aspect of testing. Instead I wanted to
understand the underlying problem and fix it.

11. The problem is that in the form submission request, the referer
domain was different from the current domain. I don't have access to
the request; it's internal to WebTest. I put print statements in
'pyramid.csrf' to determine what the request attributes were and what
the mismatch was. The actual domain was 'localhost' while the referer
had 'localhost:80'. It wasn't clear what code was calculating the
referer value or what it based it on. It didn't make sense because
didn't it know that 'localhost' is the same as 'localhost:80'?

12. Enlightenment came when I realized the referer was
"https://localhost:80", which is nonsense. The default HTTPS port is
443, not 80.

13. I tried setting extra_environ "HTTP_PORT" : "443" but that didn't
work: the referer still had "https://localhost:80".

14. I tracked down the code to the
'webob.request.BaseRequest.host_url' property. It looks first in
envvar 'HTTP_HOST' and falls back to 'SERVER_NAME' and 'SERVER_PORT'.

15. So I changed my code thus:

``extra_environ = {"HTTP_HOST": "443", "wsgi.url_scheme": "https"}
apptest.webtest.TestApp(app, extra_environ=extra_environ)''

Presto, it works!

I'm not sure if 'webtest' should be smarter, or if so exactly how. But
at least this solves this underlying problem.

Is there anything else I should do to more completely mimic an HTTPS request?
>> email to pylons-discus...@googlegroups.com.
>> To post to this group, send email to pylons-...@googlegroups.com.
> --
> You received this message because you are subscribed to the Google Groups
> "pylons-discuss" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to pylons-discus...@googlegroups.com.
> To post to this group, send email to pylons-...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/pylons-discuss/CAKdhhwFWJkrGFxOTkAyMDB61JkCm0%3D%3DwWONQ5V9U60D%3DAqV%3DXQ%40mail.gmail.com.
>
> For more options, visit https://groups.google.com/d/optout.



--
Mike Orr <slugg...@gmail.com>

Mike Orr

unread,
Oct 11, 2017, 6:53:56 PM10/11/17
to pylons-...@googlegroups.com
Correction.

15. So I changed my code thus:

``extra_environ = {"HTTP_HOST": "localhost:443", "wsgi.url_scheme": "https"}
apptest.webtest.TestApp(app, extra_environ=extra_environ)''
--
Mike Orr <slugg...@gmail.com>

Michael Merickel

unread,
Oct 11, 2017, 7:09:14 PM10/11/17
to Pylons
I think you nailed it. Now that you're setting the host header properly everything looks like it's coming from the same domain and thus none of your requests look like they're cross-origin triggering extra checks. This also fixes some things in url general I'm sure because your tests were probably were generating urls like https://localhost:80/foo instead of https://localhost/foo since your scheme is https and you set the host header to the standard https port.

- Michael

>>> email to pylons-discuss+unsubscribe@googlegroups.com.
>>> To post to this group, send email to pylons-discuss@googlegroups.com.

>>> To view this discussion on the web visit
>>> https://groups.google.com/d/msgid/pylons-discuss/CAH9f%3Duo%2BAYpm6-oS-vPLFg0Ek3gz1G2JDHcW2m0MmdH_X8zp4g%40mail.gmail.com.
>>> For more options, visit https://groups.google.com/d/optout.
>>
>>
>> --
>> You received this message because you are subscribed to the Google Groups
>> "pylons-discuss" group.
>> To unsubscribe from this group and stop receiving emails from it, send an
>> email to pylons-discuss+unsubscribe@googlegroups.com.
>> To post to this group, send email to pylons-discuss@googlegroups.com.
--
You received this message because you are subscribed to the Google Groups "pylons-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pylons-discuss+unsubscribe@googlegroups.com.
To post to this group, send email to pylons-discuss@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pylons-discuss/CAH9f%3Duq_g7H%2BW1U%3DFp75%2B1NNHjCzPxKhgomEpAFsf1_utcw-sg%40mail.gmail.com.
Reply all
Reply to author
Forward
0 new messages