diff options
Diffstat (limited to '')
| -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 | 
4 files changed, 49 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):          """  |