aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pysite/base_route.py21
-rw-r--r--pysite/constants.py6
-rw-r--r--pysite/decorators.py21
-rw-r--r--pysite/route_manager.py6
-rw-r--r--requirements.txt1
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