aurweb.asgi: add security headers middleware

This commit introduces a middleware function which adds
the following security headers to each response:

- Content-Security-Policy
    - This includes a new `nonce`, which is tied to a user
      via authentication middleware. Both an anonymous user
      and an authenticated user recieve their own random nonces.
- X-Content-Type-Options
- Referrer-Policy
- X-Frame-Options

They are then tested for existence in test/test_routes.py.

Note: The overcomplicated-looking asyncio behavior in the
middleware function is used to avoid a warning about the old
coroutine awaits being deprecated. See
https://docs.python.org/3/library/asyncio-task.html#asyncio.wait
for more detail.

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-06-12 03:54:41 -07:00
parent 13456fea1e
commit 865c414504
6 changed files with 106 additions and 3 deletions

View file

@ -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"<h1>{exc.status_code} {phrase}</h1><p>{exc.detail}</p>",
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