CORS in Django für Entwicklungs­umgebung

In diesem Beitrag geht es darum, wie man ein Django-Projekt so aufbaut, dass es nur im DEBUG-Modus CORS-Requests zuläst, auch wenn diese einen Login im Backend erfordern. Bei uns hat sich das als hilfreich herausgestellt um Frontendanpassungen an der internen Dev-Umgebung zu testen ohne dafür das Backend lokal starten zu müssen.

Es geht hier also um Projekte, bei denen in Produktion kein CORS verwendet wird, da Backend und Frontend hinter der gleichen Domain per Reverse-Proxy vereint werden.

Was ist CORS?

Ohne spezielle HTTP-Header weigern sich alle modernen Browser, Daten einer Seite an einen Server mit einer anderen Domain zu schicken. Der Mechanismus um das explizit zu freizuschalten nennt sich "Cross Origin Resource Sharing", kurz CORS. Mit dem Standardverhalten, also erst mal alles zu blocken, werden Phishing-Attacken erschwert, da das Formular zur Eingabe sensibler Daten bei der gleichen Domain gehostet werden muss, bei der die Daten empfangen werden.

Auch bei Cookies, also insbesondere dem Session-Cookie, muss man explizit dazu sagen, wenn es über unterschiedliche Seiten hinweg gültig sein soll.

Was tun?

Dieses Standardverhalten für öffentlich erreichbare Instanzen wollen wir nicht ändern. Mit der vorgeschlagenen Konfiguration vergrößert sich die Angriffsfläche zwar nur minimal, für den produktiven Einsatz dennoch unerwünscht.

Stattdessen verwenden wir die DEBUG-Flag, um die Zugriffe von 127.0.0.1 und localhost zu ermöglichen. Die Annahme ist dabei natürlich, dass DEBUG nur bei internen Instanzen gesetzt ist.

Um die CORS-Header zu setzen verwenden wir django-cors-headers (muss also installiert sein).

Wenn es produktiv keine APIs gibt, die CORS unterstützen sollen, sieht die Konfiguration so aus:

if DEBUG:
    CSRF_COOKIE_SECURE = False
    CORS_REPLACE_HTTPS_REFERER = True
    CSRF_COOKIE_DOMAIN = None
    SESSION_COOKIE_SECURE = False
    SESSION_COOKIE_HTTPONLY = False
    CORS_ALLOW_CREDENTIALS = True
    SESSION_COOKIE_SAMESITE = "None"
    CSRF_COOKIE_SAMESITE = "None"
    CORS_ALLOWED_ORIGIN_REGEXES = [
        r"^null$",
        r"^http://localhost:[0-9]+$",
        r"^http://127\\.0\\.0\\.1:[0-9]+$",
        r"^https://localhost:[0-9]+$",
        r"^https://127\\.0\\.0\\.1:[0-9]+$",
    ]
    INSTALLED_APPS = ["corsheaders"]
    MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"]
else:
    CSRF_COOKIE_SECURE = True
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    INSTALLED_APPS = []
    MIDDLEWARE = []
 
INSTALLED_APPS += [
    ...
]
 
MIDDLEWARE += [
    ...
]

Bei älteren Django-Versionen (<=3) braucht es zusätzlich noch einen Workaround um das "None" an den Cookies auch wirklich zu setzen.

Mit öffentlichen APIs mit CORS-Support macht die Unterscheidung der CORS- und Cookie-Settings weiterhin Sinn, aber die INSTALLED_APPS und MIDDLEWARE könnte man wieder zusammenlegen:

if DEBUG:
    CSRF_COOKIE_SECURE = False
    CORS_REPLACE_HTTPS_REFERER = True
    CSRF_COOKIE_DOMAIN = None
    SESSION_COOKIE_SECURE = False
    SESSION_COOKIE_HTTPONLY = False
    CORS_ALLOW_CREDENTIALS = True
    SESSION_COOKIE_SAMESITE = "None"
    CSRF_COOKIE_SAMESITE = "None"
    CORS_ALLOWED_ORIGIN_REGEXES = [
        r"^null$",
        r"^http://localhost:[0-9]+$",
        r"^http://127\\.0\\.0\\.1:[0-9]+$",
        r"^https://localhost:[0-9]+$",
        r"^https://127\\.0\\.0\\.1:[0-9]+$",
    ]
else:
    CSRF_COOKIE_SECURE = True
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    # just an example:
    CORS_ORIGIN_ALLOW_ALL = True
    CORS_URLS_REGEX = r'^/api/.*$'
 
INSTALLED_APPS += [
    "corsheaders",
    ...
]
 
MIDDLEWARE += [
    "corsheaders.middleware.CorsMiddleware"
    ...
]

Je nach Anwendung kann das natürlich auch alles noch variieren, aber hoffentlich ist mit den Code-Schnipseln oben schon mal ein Anfang gegeben.

Frontendanpassungen

Um die CORS-Funktionalität zu verwenden muss im Frontend noch (hier dann nur lokal, bzw. über Umgebungsvariablen) der Request angepasst werden.

Beispiel für fetch:

const fetchResponsePromise = fetch(resource, {"mode": "cors", "credentials": "include"})

Oder graphql-request:

const graphQLClient = new GraphQLClient(endpoint, {
  credentials: 'include',
  mode: 'cors',
})
geschrieben von Milan Oberkirch | 12.1.2022
Mehr zum Thema
9 min Lesezeit | Blog

Snapping stops to vehicle trajectories

How to snap points to a line string in a given order and what it has to do with quality assurance when importing public transport schedules.

weiterlesen
7 min Lesezeit | Blog

Using Redis Subscriptions efficiently in Python

Inspired by the websockets broadcast feature we built a subscription multiplexer for redis subscriptions to subscribe to Redis channels and patterns once for all relevant clients.

weiterlesen
5 min Lesezeit | Blog

Werkzeuge für schönere Python-Projekte

Dieser Blog-Beitrag skizziert den aktuellen Setup von pre-commit hooks, statischer Code-Analyse-Tools (Flake8, Black) und Abhängigkeitsmanagement (setuptools, pip-tools) für Python-Projekte bei geOps."

weiterlesen
8 min Lesezeit | Blog

Hinzufügen von Typ-Hinweisen zu vorhandenem Code in Python

Der Python-Interpreter behandelt Typen auf dynamische und flexible Weise ohne Einschränkungen bezüglich des Objekttyps, dem eine Variable zugewiesen ist. Seit Python 3.5 haben Programmierer die Möglichkeit, Typ-Annotationen in ihren Code einzufügen. Wir zeigen, wie man das macht.

weiterlesen
6 min Lesezeit | Blog

Webkarten als PDF exportieren und drucken

Schon seit einiger Zeit bieten einige unserer Apps den Export unserer Karten im PDF-Format. Dieser Artikel stellt unsere Lösungen für diverse Neuerungen dieser Funktion vor.

weiterlesen
2 min Lesezeit | Blog

GraphQL - Hackathon 2022

Mitglieder des geOps-Entwicklerteams erforschen GraphQL in einem internen Hackathon, um dessen Potenzial für geOps-Projekte zu entdecken.

weiterlesen

Kontakt

geOps AG
Solothurnerstrasse 235
CH-4600 Olten

fon: +41 61 588 05 05
mail: info@geops.ch
geOps GmbH
Bismarckallee 10
D-79098 Freiburg im Breisgau

fon: +49 761 458 925 0
mail: info@geops.de
Impressum | Datenschutz | Bedingungen