import base64 import secrets import importlib from flask import Flask from .common.common import * from .common.user import init_login app = Flask(__name__) app.secret_key = base64.b64decode(cf['frontend'].get('secret_key','')) or \ secrets.token_bytes(16) # development fallback; CSRF/cookies won't persist. init_login(app) for name in cf['frontend']['modules'].split(','): blueprint = importlib.import_module('.'+name, __name__) app.register_blueprint(blueprint.frontend) # TODO: should this go somewhere else? # This error handler logs requests to external apis. this makes debugging of api responses easier, as the request can be reconstructed and replayed. from flask import g from werkzeug.exceptions import InternalServerError @app.errorhandler(InternalServerError) def log_errors(e): if 'api_requests' in g: app.logger.error(g.api_requests) return e # TODO: build a proper flask extension # Magic CSRF protection: This modifies outgoing HTML responses and injects a csrf token into all forms. # All post requests are then checked if they contain the valid token. # TODO: # - don't use regex for injecting # - inject a http header into all responses (that could be used by apis) # - allow csrf token to be passed in http header, json, ... # - a decorator on routes to opt out of verification or output munging # https://stackoverflow.com/questions/19574694/flask-hit-decorator-before-before-request-signal-fires # - allow specifying hmac message contents (currently request.remote_addr) import re import hmac import hashlib from flask import request @app.after_request def add_csrf_protection(response): if response.mimetype == "text/html": token = hmac.new(app.secret_key, request.remote_addr.encode('ascii'), hashlib.sha256).hexdigest() # TODO: will fail behind reverse proxy (remote_addr always localhost) response.set_data( re.sub( rb'''(<[Ff][Oo][Rr][Mm](\s+[a-zA-Z0-9-]+(=(\w*|'[^']*'|"[^"]*"))?)*>)''', # match form tags with any number of attributes and any type of quotes rb'\1', # hackily append a hidden input with our csrf protection value response.get_data())) return response @app.before_request def verify_csrf_protection(): token = hmac.new(app.secret_key, request.remote_addr.encode('ascii'), hashlib.sha256).hexdigest() # TODO: will fail behind reverse proxy (remote_addr always localhost) if request.method == "POST" and request.form.get('csrf') != token: return "CSRF validation failed!", 400 request.form = request.form.copy() # make it mutable request.form.poplist('csrf') # remove our csrf again