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
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, um zu prüfen, ob die Variablentypen gültig sind. In diesem Blogeintrag zeigen wir Ihnen, wie man das macht.

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
2 min Lesezeit | Blog

Von Backend bis Frontend: wir suchen Verstärkung

Wir bieten zwei Stellen als Fullstack- oder Backend-Entwickler:innen für unsere Offices in Freiburg und Olten. Wenn du deine Leidenschaft in einem starken Team einbringen willst, dann solltest du dich bewerben.

weiterlesen
3 min Lesezeit | Blog

Umstellung von enzyme auf testing-library/react

Wir haben unsere Frontend-Unit-Tests von enzyme auf testing-library/react umgeschrieben. Dieser Artikel bietet einen schnellen Überblick über die Aktualisierungen.

weiterlesen
2 min Lesezeit | Blog

mapset 2.0 mit MUI

mapset 2.0 wurde unter Verwendung der Open-Source-React-Komponentenbibliothek MUI (Material-UI) umfassend überarbeitet.

weiterlesen
3 min Lesezeit | Blog

Benutzertests in Adobe XD

Was User Testing mit deinem neuen Herd zu tun hat, erklären wir hier.

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

fon: +49 761 458 925 0
mail: info@geops.de