diff --git a/aurweb/asgi.py b/aurweb/asgi.py index 861f6056..6c4d457d 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -1,6 +1,8 @@ +import asyncio import http +import typing -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.authentication import AuthenticationMiddleware @@ -55,3 +57,42 @@ async def http_exception_handler(request, exc): phrase = http.HTTPStatus(exc.status_code).phrase return HTMLResponse(f"

{exc.status_code} {phrase}

{exc.detail}

", status_code=exc.status_code) + + +@app.middleware("http") +async def add_security_headers(request: Request, call_next: typing.Callable): + """ This middleware adds the CSP, XCTO, XFO and RP security + headers to the HTTP response associated with request. + + CSP: Content-Security-Policy + XCTO: X-Content-Type-Options + RP: Referrer-Policy + XFO: X-Frame-Options + """ + response = asyncio.create_task(call_next(request)) + await asyncio.wait({response}, return_when=asyncio.FIRST_COMPLETED) + response = response.result() + + # Add CSP header. + nonce = request.user.nonce + csp = "default-src 'self'; " + script_hosts = [ + "ajax.googleapis.com", + "cdn.jsdelivr.net" + ] + csp += f"script-src 'self' 'nonce-{nonce}' " + ' '.join(script_hosts) + response.headers["Content-Security-Policy"] = csp + + # Add XTCO header. + xcto = "nosniff" + response.headers["X-Content-Type-Options"] = xcto + + # Add Referrer Policy header. + rp = "same-origin" + response.headers["Referrer-Policy"] = rp + + # Add X-Frame-Options header. + xfo = "SAMEORIGIN" + response.headers["X-Frame-Options"] = xfo + + return response diff --git a/aurweb/auth.py b/aurweb/auth.py index f57e18bf..ba5f0fea 100644 --- a/aurweb/auth.py +++ b/aurweb/auth.py @@ -10,7 +10,7 @@ from starlette.requests import HTTPConnection import aurweb.config -from aurweb import l10n +from aurweb import l10n, util from aurweb.models.session import Session from aurweb.models.user import User from aurweb.templates import make_variable_context, render_template @@ -25,6 +25,12 @@ class AnonymousUser: # A stub ssh_pub_key relationship. ssh_pub_key = None + # A nonce attribute, needed for all browser sessions; set in __init__. + nonce = None + + def __init__(self): + self.nonce = util.make_nonce() + @staticmethod def is_authenticated(): return False @@ -55,7 +61,9 @@ class BasicAuthBackend(AuthenticationBackend): # exists, due to ForeignKey constraints in the schema upheld # by mysqlclient. user = session.query(User).filter(User.ID == record.UsersID).first() + user.nonce = util.make_nonce() user.authenticated = True + return AuthCredentials(["authenticated"]), user diff --git a/aurweb/models/user.py b/aurweb/models/user.py index 83cde5f1..9db9add0 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -37,6 +37,7 @@ class User(Base): # High-level variables used to track authentication (not in DB). authenticated = False + nonce = None def __init__(self, Passwd: str = str(), **kwargs): super().__init__(**kwargs) diff --git a/aurweb/util.py b/aurweb/util.py index b34226a2..e5f510ce 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,6 +1,8 @@ import base64 +import math import random import re +import secrets import string from collections import OrderedDict @@ -20,6 +22,15 @@ def make_random_string(length): string.digits, k=length)) +def make_nonce(length: int = 8): + """ Generate a single random nonce. Here, token_hex generates a hex + string of 2 hex characters per byte, where the length give is + nbytes. This means that to get our proper string length, we need to + cut it in half and truncate off any remaining (in the case that + length was uneven). """ + return secrets.token_hex(math.ceil(length / 2))[:length] + + def valid_username(username): min_len = aurweb.config.getint("options", "username_min_len") max_len = aurweb.config.getint("options", "username_max_len") diff --git a/templates/partials/typeahead.html b/templates/partials/typeahead.html index d943dbc4..c218b8d1 100644 --- a/templates/partials/typeahead.html +++ b/templates/partials/typeahead.html @@ -1,6 +1,6 @@ -