enable CSRF protection in existing balrog code

RESOLVED FIXED

Status

Release Engineering
General Automation
P3
normal
RESOLVED FIXED
6 years ago
5 years ago

People

(Reporter: bhearsum, Assigned: bhearsum)

Tracking

Firefox Tracking Flags

(Not tracked)

Details

(Whiteboard: [balrog])

Attachments

(4 attachments, 2 obsolete attachments)

(Assignee)

Description

6 years ago
The security team says that we need to have CSRF protection for Balrog, even though it's behind a VPN. There's already some code landed that doesn't have it, and bug 672918 will add a bunch more. We should add csrf protection for existing code as soon as we can, and make sure all new code uses it, too.
(Assignee)

Comment 1

6 years ago
http://packages.python.org/Flask-WTF/ seems to say that we only need to put it on our forms, and then make sure it's passed with AJAX requests.
Priority: -- → P3
Whiteboard: [balrog]
(Assignee)

Updated

6 years ago
Duplicate of this bug: 741354
We will need to update the csrf token on the admin-ui pages when the form is submitted.
Currently we can only do one 'submit' per reload of the page. If we make a mistake on a form, or want to do multiple operations, we need to reload.

We shouldn't need to reload for each form submission.
(Assignee)

Comment 4

6 years ago
Catlee suggested that we could return a new csrf token in the HTTP headers of PUT/POST/DELETE responses. Eg, X-CSRF-TOKEN or something. That seems a bit nicer than mixing it in with the real data.
(Assignee)

Comment 5

6 years ago
I looked into this further today and found that the latest versions of WTForms and Flask-WTF don't require csrf token regeneration with every request - they use an hmac digest that's based on the secret key to generate the token, and set a relatively short expiry time. This seems like a much easier way to do things...I'm going to try upgrading us.
Assignee: nobody → bhearsum
(Assignee)

Comment 6

6 years ago
Created attachment 628901 [details] [diff] [review]
client csrf support

I think this patch is mostly straightforward. I did a little bit of refactoring, to make API a proper base class, and taught it how to get CSRF tokens and data versions from headers. I also had to use a session because the WTForms CSRF protection puts the hmac key in it.
Attachment #628901 - Flags: feedback?(rail)
(Assignee)

Comment 7

6 years ago
Created attachment 628903 [details] [diff] [review]
upgrade flask, wtforms, flask-wtf; fix up server side csrf implementation

I'm not sure I'm completely happy with this, as it muddies are usage of csrf_token and data_version a bit, so I'm curious what you think.

For templates/UI, we're still putting hidden csrf_token and data_version fields onto the pages. For API endpoints (eg, /releases/:release/builds/:platform/:locale), we return them in X-CSRF-Token and X-Data-Version headers. I was going to put the data version in the response body instead, but most of those methods don't have a nice place to put it, because they return parts or all of a blob that aren't wrapped with a variable name. Eg, returning {'complete': {...}, 'data_version': 1} seems a bit awkward. However, I'm not sure it makes sense to put data version in a header either...

What do you think?
Attachment #628903 - Flags: feedback?(catlee)
Comment on attachment 628901 [details] [diff] [review]
client csrf support

Review of attachment 628901 [details] [diff] [review]:
-----------------------------------------------------------------

::: lib/python/balrog/client/api.py
@@ +13,5 @@
>  
> +def is_csrf_token_expired(token):
> +    from datetime import datetime
> +    expiry = token.split('##')[0]
> +    if expiry <= datetime.now().strftime('%Y%m%d%H%M%S'):

* This function isn't used anywhere. Do you need it?
* split and strftime may raise exceptions, be careful :)
Attachment #628901 - Flags: feedback?(rail) → feedback+
(Assignee)

Comment 9

6 years ago
(In reply to Rail Aliiev [:rail] from comment #8)
> Comment on attachment 628901 [details] [diff] [review]
> client csrf support
> 
> Review of attachment 628901 [details] [diff] [review]:
> -----------------------------------------------------------------
> 
> ::: lib/python/balrog/client/api.py
> @@ +13,5 @@
> >  
> > +def is_csrf_token_expired(token):
> > +    from datetime import datetime
> > +    expiry = token.split('##')[0]
> > +    if expiry <= datetime.now().strftime('%Y%m%d%H%M%S'):
> 
> * This function isn't used anywhere. Do you need it?
> * split and strftime may raise exceptions, be careful :)

Whoops, I forgot to re-add expiration checking after I refactored!
(Assignee)

Comment 10

6 years ago
Created attachment 629181 [details] [diff] [review]
check expiration of csrf token
Attachment #628901 - Attachment is obsolete: true
Attachment #629181 - Flags: review?(rail)
Attachment #629181 - Flags: review?(rail) → review+
Comment on attachment 628903 [details] [diff] [review]
upgrade flask, wtforms, flask-wtf; fix up server side csrf implementation

Review of attachment 628903 [details] [diff] [review]:
-----------------------------------------------------------------

looks good to me!

::: auslib/web/views/__init__.py
@@ +3,3 @@
>  from auslib.web.views.permissions import *
>  from auslib.web.views.releases import *
>  from auslib.web.views.rules import *

import *'s are bad...how hard would it be to import views explicitly?

::: requirements/prod.txt
@@ +1,4 @@
> +flask==0.8
> +Werkzeug==0.8.3
> +wtforms==1.0.1
> +flask-wtf==0.6

are these required even though they're available in the vendor lib?
Attachment #628903 - Flags: feedback?(catlee) → feedback+
(Assignee)

Comment 12

6 years ago
Created attachment 630192 [details] [diff] [review]
move routing to avoid import *

(In reply to Chris AtLee [:catlee] from comment #11)
> Comment on attachment 628903 [details] [diff] [review]
> upgrade flask, wtforms, flask-wtf; fix up server side csrf implementation
> 
> Review of attachment 628903 [details] [diff] [review]:
> -----------------------------------------------------------------
> 
> looks good to me!
> 
> ::: auslib/web/views/__init__.py
> @@ +3,3 @@
> >  from auslib.web.views.permissions import *
> >  from auslib.web.views.releases import *
> >  from auslib.web.views.rules import *
> 
> import *'s are bad...how hard would it be to import views explicitly?

Easy enough to fix.

> ::: requirements/prod.txt
> @@ +1,4 @@
> > +flask==0.8
> > +Werkzeug==0.8.3
> > +wtforms==1.0.1
> > +flask-wtf==0.6
> 
> are these required even though they're available in the vendor lib?

I'm using the model described here: https://github.com/mozilla/zamboni-lib. I believe the idea is that prod.txt contains the list of libraries in your vendor lib. You use this file when you build it, eg:
pip install -I --install-option="--home=`pwd`/vendor" --src='vendor/src' -r requirements/prod.txt
Attachment #628903 - Attachment is obsolete: true
Attachment #630192 - Flags: review?(catlee)
(Assignee)

Updated

6 years ago
Attachment #629181 - Flags: checked-in+
Attachment #630192 - Flags: review?(catlee) → review+
(Assignee)

Comment 13

6 years ago
Comment on attachment 630192 [details] [diff] [review]
move routing to avoid import *

Jenkins run is here: https://jenkins.mozilla.org/job/Balrog/90/
I restarted the apache daemons on the web heads, to work around bug 745890.
Attachment #630192 - Flags: checked-in+
(Assignee)

Comment 14

6 years ago
Created attachment 630285 [details] [diff] [review]
add csrf/data_version to /releases/:name endpoint

I missed an edge case in my original patch. It didn't work in the case that the release existed but the platform didn't. It would receive a 404 from the initial HEAD request, and then send a HEAD to /csrf_token. However, because it never received a data version it eventually got an HTTP 400 when trying to PUT because it was missing.

This patch adds those headers to /releases/:name, so we can retrieve them in that case.
Attachment #630285 - Flags: review?(catlee)
(Assignee)

Comment 15

6 years ago
Created attachment 630286 [details] [diff] [review]
use a different url to retrieve data_version/csrf_token

This patch makes it possible to retrieve the csrf token and data version from a different URL than we're submitting to. We need this to support the scenario described in the previous comment.
Attachment #630286 - Flags: review?(rail)
Attachment #630286 - Flags: review?(rail) → review+
Comment on attachment 630285 [details] [diff] [review]
add csrf/data_version to /releases/:name endpoint

Review of attachment 630285 [details] [diff] [review]:
-----------------------------------------------------------------

::: admin.py
@@ +17,5 @@
>      )
>  
>      parser.add_option("-d", "--db", dest="db", help="database to use, relative to inputdir")
>      parser.add_option("-p", "--port", dest="port", type="int", help="port for server")
> +    parser.add_option("--host", dest="host", default='127.0.0.1', help="host for server")

this is the address the server will be listening on, right? e.g. you could have a public ip or 0.0.0.0 to listen on all interfaces?

if so, can you adjust the help string to indicate that more clearly?
Attachment #630285 - Flags: review?(catlee) → review+
(Assignee)

Comment 17

6 years ago
Comment on attachment 630286 [details] [diff] [review]
use a different url to retrieve data_version/csrf_token

Once the Balrog patch gets reviewed/landed we should have updates on Linux/Mac every day again.
Attachment #630286 - Flags: checked-in+
(Assignee)

Comment 18

6 years ago
Comment on attachment 630285 [details] [diff] [review]
add csrf/data_version to /releases/:name endpoint

I fixed up the help string and landed this: https://jenkins.mozilla.org/job/Balrog/91/

After the webheads pick it up I'll restart them to pick up the changes, and then resubmit any failed mozilla-central submissions from this morning.
Attachment #630285 - Flags: checked-in+
(Assignee)

Comment 19

6 years ago
Okay, things seem to be working now. Sample update URLs:
https://aus4-dev.allizom.org/update/3/Firefox/15.0a1/20120505030510/WINNT_x86-msvc/en-US/nightly/Linux%203.2.0-24-generic%20%28GTK%202.24.10%29/default/default/update.xml?force=1
https://aus4-dev.allizom.org/update/3/Firefox/15.0a1/20120505030510/Linux_x86_64-gcc3/en-US/nightly/Linux%203.2.0-24-generic%20%28GTK%202.24.10%29/default/default/update.xml?force=1
https://aus4-dev.allizom.org/update/3/Firefox/15.0a1/20120505030510/Linux_x86-gcc3/en-US/nightly/Linux%203.2.0-24-generic%20%28GTK%202.24.10%29/default/default/update.xml?force=1
https://aus4-dev.allizom.org/update/3/Firefox/15.0a1/20120505030510/Darwin_x86-gcc3-u-i386-x86_64/en-US/nightly/Linux%203.2.0-24-generic%20%28GTK%202.24.10%29/default/default/update.xml?force=1

I had to rerun the balrog client by hand for a few of them because my patches didn't land in time to be picked up. Subsequent nightlies should work from the get-go, though. We even have working Windows updates now, because they moved to 64-bit machines. (Previously they were broken because the 32-bit windows build machines weren't using a new enough Python).
Status: NEW → RESOLVED
Last Resolved: 6 years ago
Resolution: --- → FIXED
Product: mozilla.org → Release Engineering
You need to log in before you can comment on or make changes to this bug.