Skip to content
This repository has been archived by the owner on Jul 7, 2022. It is now read-only.

Commit

Permalink
csrf for react (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
djrobstep authored Nov 14, 2016
1 parent 2dad4a2 commit 90a3572
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 9 deletions.
24 changes: 24 additions & 0 deletions dmutils/csrf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os
import binascii
from flask import session, request


TOKEN = '_csrf_token'
REACT_HEADER_NAME = 'X-CSRFToken'


def random_string(length=32):
return binascii.b2a_hex(os.urandom(length)).decode('utf-8')


def get_csrf_token():
if TOKEN not in session:
session[TOKEN] = random_string()
return session[TOKEN]


def check_valid_header_csrf():
try:
return session[TOKEN] == request.headers[REACT_HEADER_NAME]
except KeyError:
return False
17 changes: 13 additions & 4 deletions dmutils/flask_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@

import flask_featureflags
from . import config, logging, force_https, request_id, formats, filters
from flask import Markup, redirect, request, session
from flask_script import Manager, Server
from flask import Markup, redirect, request, session, current_app, abort
from flask.ext.script import Manager, Server
from flask_login import current_user
from werkzeug.contrib.fixers import ProxyFix

from .asset_fingerprint import AssetFingerprinter
from .user import User, user_logging_string

from dmutils import terms_of_use
from dmutils.forms import valid_csrf_or_abort
from dmutils.forms import is_csrf_token_valid

from .csrf import check_valid_header_csrf


def init_app(
Expand Down Expand Up @@ -97,7 +99,14 @@ def load_user(user_id):
@application.before_request
def check_csrf_token():
if request.method in ('POST', 'PATCH', 'PUT', 'DELETE'):
valid_csrf_or_abort()
flask_csrf_valid = is_csrf_token_valid()
react_csrf_valid = check_valid_header_csrf()

if not (flask_csrf_valid or react_csrf_valid):
current_app.logger.info(
u'csrf.invalid_token: Aborting request, user_id: {user_id}',
extra={'user_id': session.get('user_id', '<unknown')})
abort(400, 'Invalid CSRF token. Please try again.')

@application.before_request
def refresh_session():
Expand Down
8 changes: 7 additions & 1 deletion react/render_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask import request

from .exceptions import ReactRenderingError, RenderServerError
from dmutils.csrf import get_csrf_token

from six import python_2_unicode_compatible

Expand All @@ -16,7 +17,7 @@ def __init__(self, markup, props, slug=None, files=None):
self.markup = markup
self.props = props
self.slug = slug
self.files = files
self.files = files or {}

def __str__(self):
return self.markup
Expand Down Expand Up @@ -46,6 +47,11 @@ def render(self, path, props=None, to_static_markup=False, request_headers=None)
if props is None:
props = {}

if 'form_options' not in props:
props['form_options'] = {}

props['form_options']['csrf_token'] = get_csrf_token()

# Add default options.
opts = props.get('options', {})
opts.update({
Expand Down
13 changes: 13 additions & 0 deletions tests/test_flask_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ def test_csrf_wrong(self):
)
assert res.status_code == 400

def test_alternate_csrf(self):
with self.app.session_transaction() as sess:
sess['_csrf_token'] = 'abc123'

res = self.app.post(
'/thing',
data={'csrf_token': 'nope'},
headers={
'X-CSRFToken': 'abc123'
}
)
assert res.status_code == 200


class TestTemplateFilters(BaseApplicationTest):

Expand Down
14 changes: 10 additions & 4 deletions tests/test_render_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ class TestRenderServer(BaseApplicationTest):

@patch('react.render_server.hashlib')
@patch('react.render_server.requests')
def test_render_server_success(self, requests, hashlib):
@patch('react.render_server.get_csrf_token')
def test_render_server_success(self, get_csrf_token, requests, hashlib):
get_csrf_token.return_value = 'abc123'

with self.flask.test_request_context('/test'):
sha = sha1()
hashlib.sha1.return_value = sha
Expand All @@ -43,20 +46,23 @@ def test_render_server_success(self, requests, hashlib):
headers={'content-type': 'application/json'},
params={'hash': sha.hexdigest()},
data='{"path": "' + path + '", ''"serializedProps": "{\\"_serverContext\\": '
'{\\"location\\": \\"/test\\"}, '
'{\\"location\\": \\"/test\\"}, \\"form_options\\": {\\"csrf_token\\": \\"abc123\\"}, '
'\\"options\\": '
'{\\"apiUrl\\": \\"http://api\\", \\"serverRender\\": true}}", '
'"toStaticMarkup": false}'
)

def test_react_render_not_set(self):
@patch('react.render_server.get_csrf_token')
def test_react_render_not_set(self, get_csrf_token):
get_csrf_token.return_value = 'abc123'

self.flask.config.update({'REACT_RENDER': None})

with self.flask.test_request_context('/test'):
result = render_server.render('/widget/component.js')
assert result.render() == ''
assert result.get_props() == '{"_serverContext": ' \
'{"location": "/test"}, ' \
'{"location": "/test"}, "form_options": {"csrf_token": "abc123"}, ' \
'"options": {"apiUrl": "http://api", ' \
'"serverRender": true}}'

Expand Down

0 comments on commit 90a3572

Please sign in to comment.