Fix and unify CSRF support across web and API endpoints.
This commit is contained in:
parent
0097daebc2
commit
f060fd6ae0
5 changed files with 53 additions and 28 deletions
|
@ -18,6 +18,7 @@ from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermissi
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from auth.auth_context import get_authenticated_user, get_validated_oauth_token
|
from auth.auth_context import get_authenticated_user, get_validated_oauth_token
|
||||||
from auth.auth import process_oauth
|
from auth.auth import process_oauth
|
||||||
|
from endpoints.csrf import csrf_protect
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -25,6 +26,7 @@ api_bp = Blueprint('api', __name__)
|
||||||
api = Api()
|
api = Api()
|
||||||
api.init_app(api_bp)
|
api.init_app(api_bp)
|
||||||
api.decorators = [process_oauth,
|
api.decorators = [process_oauth,
|
||||||
|
csrf_protect,
|
||||||
crossdomain(origin='*', headers=['Authorization', 'Content-Type'])]
|
crossdomain(origin='*', headers=['Authorization', 'Content-Type'])]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
import urlparse
|
import urlparse
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import session, make_response, render_template, request
|
from flask import make_response, render_template, request
|
||||||
from flask.ext.login import login_user, UserMixin, current_user
|
from flask.ext.login import login_user, UserMixin
|
||||||
from flask.ext.principal import identity_changed
|
from flask.ext.principal import identity_changed
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
|
@ -85,15 +83,6 @@ def handle_dme(ex):
|
||||||
return make_response(json.dumps({'message': ex.message}), 400)
|
return make_response(json.dumps({'message': ex.message}), 400)
|
||||||
|
|
||||||
|
|
||||||
def generate_csrf_token():
|
|
||||||
if '_csrf_token' not in session:
|
|
||||||
session['_csrf_token'] = base64.b64encode(os.urandom(48))
|
|
||||||
|
|
||||||
return session['_csrf_token']
|
|
||||||
|
|
||||||
app.jinja_env.globals['csrf_token'] = generate_csrf_token
|
|
||||||
|
|
||||||
|
|
||||||
def render_page_template(name, **kwargs):
|
def render_page_template(name, **kwargs):
|
||||||
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), **kwargs))
|
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), **kwargs))
|
||||||
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||||
|
|
43
endpoints/csrf.py
Normal file
43
endpoints/csrf.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import session, request
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
from auth.auth_context import get_validated_oauth_token
|
||||||
|
from util.http import abort
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_csrf_token():
|
||||||
|
if '_csrf_token' not in session:
|
||||||
|
session['_csrf_token'] = base64.b64encode(os.urandom(48))
|
||||||
|
|
||||||
|
return session['_csrf_token']
|
||||||
|
|
||||||
|
|
||||||
|
def csrf_protect(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
oauth_token = get_validated_oauth_token()
|
||||||
|
if oauth_token is None and request.method != "GET" and request.method != "HEAD":
|
||||||
|
token = session.get('_csrf_token', None)
|
||||||
|
found_token = request.values.get('_csrf_token', None)
|
||||||
|
|
||||||
|
# TODO: add if not token here, once we are sure all sessions have a token.
|
||||||
|
if token != found_token:
|
||||||
|
msg = 'CSRF Failure. Session token was %s and request token was %s'
|
||||||
|
logger.error(msg, token, found_token)
|
||||||
|
abort(403, message='CSRF token was invalid or missing.')
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
logger.warning('No CSRF token in session.')
|
||||||
|
else:
|
||||||
|
logger.debug('Found and validated CSRF token.')
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
app.jinja_env.globals['csrf_token'] = generate_csrf_token
|
|
@ -14,7 +14,8 @@ from auth.permissions import AdministerOrganizationPermission
|
||||||
from util.invoice import renderInvoiceToPdf
|
from util.invoice import renderInvoiceToPdf
|
||||||
from util.seo import render_snapshot
|
from util.seo import render_snapshot
|
||||||
from util.cache import no_cache
|
from util.cache import no_cache
|
||||||
from endpoints.common import common_login, render_page_template, generate_csrf_token
|
from endpoints.common import common_login, render_page_template
|
||||||
|
from endpoints.csrf import csrf_protect, generate_csrf_token
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
from util.gravatar import compute_hash
|
from util.gravatar import compute_hash
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
@ -248,6 +249,7 @@ class FlaskAuthorizationProvider(DatabaseAuthorizationProvider):
|
||||||
|
|
||||||
|
|
||||||
@web.route('/oauth/authorizeapp', methods=['POST'])
|
@web.route('/oauth/authorizeapp', methods=['POST'])
|
||||||
|
@csrf_protect
|
||||||
def authorize_application():
|
def authorize_application():
|
||||||
if not current_user.is_authenticated():
|
if not current_user.is_authenticated():
|
||||||
abort(401)
|
abort(401)
|
||||||
|
@ -257,18 +259,13 @@ def authorize_application():
|
||||||
client_id = request.form.get('client_id', None)
|
client_id = request.form.get('client_id', None)
|
||||||
redirect_uri = request.form.get('redirect_uri', None)
|
redirect_uri = request.form.get('redirect_uri', None)
|
||||||
scope = request.form.get('scope', None)
|
scope = request.form.get('scope', None)
|
||||||
csrf = request.form.get('csrf', None)
|
|
||||||
|
|
||||||
# Verify the csrf token.
|
|
||||||
if csrf != generate_csrf_token():
|
|
||||||
abort(404)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Add the access token.
|
# Add the access token.
|
||||||
return provider.get_token_response('token', client_id, redirect_uri, scope=scope)
|
return provider.get_token_response('token', client_id, redirect_uri, scope=scope)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/oauth/denyapp', methods=['POST'])
|
@web.route('/oauth/denyapp', methods=['POST'])
|
||||||
|
@csrf_protect
|
||||||
def deny_application():
|
def deny_application():
|
||||||
if not current_user.is_authenticated():
|
if not current_user.is_authenticated():
|
||||||
abort(401)
|
abort(401)
|
||||||
|
@ -278,12 +275,6 @@ def deny_application():
|
||||||
client_id = request.form.get('client_id', None)
|
client_id = request.form.get('client_id', None)
|
||||||
redirect_uri = request.form.get('redirect_uri', None)
|
redirect_uri = request.form.get('redirect_uri', None)
|
||||||
scope = request.form.get('scope', None)
|
scope = request.form.get('scope', None)
|
||||||
csrf = request.form.get('csrf', None)
|
|
||||||
|
|
||||||
# Verify the csrf token.
|
|
||||||
if csrf != generate_csrf_token():
|
|
||||||
abort(404)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Add the access token.
|
# Add the access token.
|
||||||
return provider.get_auth_denied_response('token', client_id, redirect_uri, scope=scope)
|
return provider.get_auth_denied_response('token', client_id, redirect_uri, scope=scope)
|
||||||
|
|
|
@ -54,13 +54,13 @@
|
||||||
<input type="hidden" name="client_id" value="{{ client_id }}">
|
<input type="hidden" name="client_id" value="{{ client_id }}">
|
||||||
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
|
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
|
||||||
<input type="hidden" name="scope" value="{{ scope }}">
|
<input type="hidden" name="scope" value="{{ scope }}">
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token_val }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token_val }}">
|
||||||
<button type="submit" class="btn btn-success">Authorize Application</button>
|
<button type="submit" class="btn btn-success">Authorize Application</button>
|
||||||
</form><form method="post" action="/oauth/denyapp">
|
</form><form method="post" action="/oauth/denyapp">
|
||||||
<input type="hidden" name="client_id" value="{{ client_id }}">
|
<input type="hidden" name="client_id" value="{{ client_id }}">
|
||||||
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
|
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
|
||||||
<input type="hidden" name="scope" value="{{ scope }}">
|
<input type="hidden" name="scope" value="{{ scope }}">
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token_val }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token_val }}">
|
||||||
<button type="submit" class="btn btn-default">Cancel</button>
|
<button type="submit" class="btn btn-default">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in a new issue