diff options
-rw-r--r-- | pysite/base_route.py | 21 | ||||
-rw-r--r-- | pysite/constants.py | 6 | ||||
-rw-r--r-- | pysite/decorators.py | 21 | ||||
-rw-r--r-- | pysite/route_manager.py | 6 | ||||
-rw-r--r-- | requirements.txt | 1 |
5 files changed, 50 insertions, 5 deletions
diff --git a/pysite/base_route.py b/pysite/base_route.py index 95bf3a03..494875ed 100644 --- a/pysite/base_route.py +++ b/pysite/base_route.py @@ -23,6 +23,27 @@ class BaseView(MethodView, OauthMixin): """ Render some templates and get them back in a form that you can simply return from your view function. + Here's what's inserted: + * "current_page" - the "name" attribute from the view class + * "view" - the view class instance + * "logged_in" - a boolean, True if the user is logged in + * "static_file(filename)", a function used to get the URL for a given static file + * "csrf_token()", a function returning the CSRF token stored in the current session + + For XSS protection, a CSRF token must be used. The "csrf_token()" function returns the correct token + to be used in the current rendering context - if your view methods are to be protected from XSS + exploits, the following steps must be taken: + + 1. Apply the "csrf" decorator to the view method + 2. For forms, a hidden input must be declared in the template, with the name "csrf_token", and the value set to + the CSRF token. + 3. For any AJAX work, the CSRF token should be stored in a variable, and sent as part of the request headers. + You can set the "X-CSRFToken" header to the CSRF token for this. + + Any API call or form submission not protected by an API key must not be vulnerable to XSS, unless the API + call is intended to be a completely public feature. Public API methods must not be account-bound, and they + must never return information on a current user or perform any action. Only data retrieval is permissible. + :param template_names: Names of the templates to render :param context: Extra data to pass into the template :return: String representing the rendered templates diff --git a/pysite/constants.py b/pysite/constants.py index 921a32b9..69633127 100644 --- a/pysite/constants.py +++ b/pysite/constants.py @@ -3,6 +3,8 @@ from enum import Enum, IntEnum from os import environ +from flask_wtf import CSRFProtect + class ErrorCodes(IntEnum): unknown_route = 0 @@ -68,3 +70,7 @@ PAPERTRAIL_PORT = int(environ.get("PAPERTRAIL_PORT") or 0) # DataDog logging DATADOG_ADDRESS = environ.get("DATADOG_ADDRESS") or None DATADOG_PORT = int(environ.get("DATADOG_PORT") or 0) + +# CSRF + +CSRF = CSRFProtect() diff --git a/pysite/decorators.py b/pysite/decorators.py index 8abde932..d678a8b4 100644 --- a/pysite/decorators.py +++ b/pysite/decorators.py @@ -8,7 +8,21 @@ from schema import Schema, SchemaError from werkzeug.exceptions import Forbidden from pysite.base_route import APIView, BaseView -from pysite.constants import ErrorCodes, ValidationTypes +from pysite.constants import ErrorCodes, ValidationTypes, CSRF + + +def csrf(f): + """ + Apply CSRF protection to a specific view function. + """ + + @wraps(f) + def inner_decorator(*args, **kwargs): + CSRF.protect() + + return f(*args, **kwargs) + + return inner_decorator def require_roles(*roles: int): @@ -41,12 +55,12 @@ def api_key(f): """ @wraps(f) - def inner(self: APIView, *args, **kwargs): + def inner_decorator(self: APIView, *args, **kwargs): if not request.headers.get("X-API-Key") == os.environ.get("BOT_API_KEY"): return self.error(ErrorCodes.invalid_api_key) return f(self, *args, **kwargs) - return inner + return inner_decorator def api_params(schema: Schema, validation_type: ValidationTypes = ValidationTypes.json): @@ -59,6 +73,7 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType This data will always be a list, and view functions are expected to be able to handle that in the case of multiple sets of data being provided by the api. """ + def inner_decorator(f): @wraps(f) diff --git a/pysite/route_manager.py b/pysite/route_manager.py index f8a7515e..ec0a84e3 100644 --- a/pysite/route_manager.py +++ b/pysite/route_manager.py @@ -10,8 +10,8 @@ from flask_sockets import Sockets from pysite.base_route import APIView, BaseView, ErrorView, RouteView from pysite.constants import ( - DISCORD_OAUTH_AUTHORIZED, DISCORD_OAUTH_ID, DISCORD_OAUTH_REDIRECT, DISCORD_OAUTH_SCOPE, DISCORD_OAUTH_SECRET, - PREFERRED_URL_SCHEME) + CSRF, DISCORD_OAUTH_AUTHORIZED, DISCORD_OAUTH_ID, DISCORD_OAUTH_REDIRECT, DISCORD_OAUTH_SCOPE, + DISCORD_OAUTH_SECRET, PREFERRED_URL_SCHEME) from pysite.database import RethinkDB from pysite.oauth import OauthBackend from pysite.websockets import WS @@ -34,6 +34,7 @@ class RouteManager: self.app.secret_key = os.environ.get("WEBPAGE_SECRET_KEY", "super_secret") self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "pythondiscord.local:8080") self.app.config["PREFERRED_URL_SCHEME"] = PREFERRED_URL_SCHEME + self.app.config["WTF_CSRF_CHECK_DEFAULT "] = False # We only want to protect specific routes self.app.before_request(self.db.before_request) self.app.teardown_request(self.db.teardown_request) @@ -80,6 +81,7 @@ class RouteManager: self.sockets.register_blueprint(self.ws_blueprint, url_prefix="/ws") self.app.before_request(self.https_fixing_hook) # Try to fix HTTPS issues + CSRF.init_app(self.app) # Set up CSRF protection def https_fixing_hook(self): """ diff --git a/requirements.txt b/requirements.txt index 2717d1e5..0ed93742 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ schema flask_sockets Flask-Dance logmatic-python +flask-wtf |