Caddy Server, CORS und Preflight-Anfragen

Gepostet am 13. April 2023  •  4 Minuten  • 733 Wörter  • Andere Sprachen:  English

Ich verwende in letzter Zeit des Öfteren den in Go geschriebenen Caddy Server . Lange war ich nginx treu, aber ich begeistert, wie leicht und schnell man in Caddy Webseiten einrichten kann und sich dabei einiges an Boilerplate und Arbeit spart. Caddy kümmert sich um das Erstellen von Zertifikaten (z.B: bei Lets Encrypt ), um automatische Umleitungen von HTTP auf HTTPs, kann HTTP3 und ist in der Regel mit ein paar Zeilen Konfiguration eingerichtet.

Cross-Site-Scripting, CORS, Preflight-Requests

Ein Problem hatte ich allerdings, als ich Caddy als API-Gateway verwenden wollte und ich auf Cross-Origin-Probleme gestoßen bin. Solche Probleme treten immer dann auf, wenn ich von einer Domain (z.B. auxnet.de) auf eine andere per Javascript zugreifen will (z.B. auf api.auxnet.de). Aus Sicherheitsgründen schicken Browser ein sogenanntes Preflight Request, technisch eine HTTP-Anfrage mit der OPTIONS-Methode. Auf diese muss ein Server korrekt reagieren, ansonsten gibt es eine Fehlermeldung in der Konsole. Erst, wenn die Preflight-Anfrage korrekt beantwortet wurde, startet der Browser die eigentliche Anfrage (z.B. ein API-Request).

CORS error in Chromium

Die Gründe für dieses Vorgehen liegen in der Sicherheit. CORS (Cross-Origin Resource Sharing) ist ein Mechanismus, der verhindern soll, dass Internet-Ressourcen nur von unberechtigten Parteien benutzt werden. Details zu dem Verfahren findet man unter anderem im Mozilla Developer Network oder in Blog-Beiträgen .

Caddy und CORS

Caddy macht ziemlich viel automatisch, in Sachen CORS muss man allerdings selbst Hand anlegen. Dies ist insofern nicht verwunderlich, Caddy kann ja schlecht wissen, welche Einstellungen im Speziellen für eine Website nötig sind.

Wir benötigen zwei Komponenten:

Kommen wir zur ersten. Ein Browser erwartet eine Antwort ohne Inhalt, vulgo 204 No Content. Dies lässt sich in Caddy folgendermaßen bewerkstelligen:

api.auxnet.de {
    @cors_preflight {
		method OPTIONS
	}
	respond @cors_preflight 204
}

Wir erstellen also einen named matcher , der auf Anfragen der OPTIONS-Methode reagieren kann. Als Zweites erstellen wir die Direktive respond , welche auf den Matcher reagiert und schlicht mit 204 antwortet.

Soweit so gut. Jetzt fehlen die sogenannten CORS-Header, von denen es einen ganzen Zoo zu geben scheint. Relevant sind in der Regel allerdings nur drei bis vier, die auf jeden Fall gesetzt werden sollten:

Erweitern wir also unsere hypothetische Konfiguration von oben:

api.auxnet.de {
    @cors_preflight {
		method OPTIONS
	}
	respond @cors_preflight 204
	
    header {
        Access-Control-Allow-Origin https://www.auxnet.de
        Access-Control-Allow-Methods GET,POST,OPTIONS,HEAD,PATCH,PUT,DELETE
        Access-Control-Allow-Headers User-Agent,Content-Type,X-Api-Key
        Access-Control-Max-Age 86400
    }
}

Wir können unsere Konfiguration mithilfe von curl testen:

curl -I -XOPTIONS https://api.auxnet.de/

Die Ausgabe sollte folgende Header enthalten (die Reihenfolge ist verändert, da Go die Schlüssel der Map alphabetisch ausgibt):

Access-Control-Allow-Headers: User-Agent,Content-Type,X-Api-Key
Access-Control-Allow-Methods: GET,POST,OPTIONS,HEAD,PATCH,PUT,DELETE
Access-Control-Allow-Origin: https://www.auxnet.de
Access-Control-Max-Age: 86400

Die Header werden übrigens bei jeder Antwort mitgeschickt, also auch bei GET oder POST-Requests. Browser benötigen die CORS-Header also bei jeder Art von Anfrage, nicht nur beim OPTIONS-Request.

Zugriff von mehreren Domains aus

Es kann mitunter vorkommen, dass man den Zugriff von mehreren Domains aus erlauben will. Access-Control-Allow-Origin erlaubt jedoch nur genau eine Domain oder eben * für alle. Mit weiteren named matchers lässt sich das Problem beheben:

api.auxnet.de {
    @cors_preflight {
		method OPTIONS
	}
	respond @cors_preflight 204
	
	@origin1 {
        header Origin https://www.auxnet.de
	}
	header @origin1 {
        Access-Control-Allow-Origin https://www.auxnet.de
	}
	@origin2 {
        header Origin https://auxnet.de
	}
	header @origin2 {
        Access-Control-Allow-Origin https://auxnet.de
	}
	
    header {
        Access-Control-Allow-Methods GET,POST,OPTIONS,HEAD,PATCH,PUT,DELETE
        Access-Control-Allow-Headers User-Agent,Content-Type,X-Api-Key
        Access-Control-Max-Age 86400
    }
}

Hier wird ein Header je nach Origin (also anfragender Domain) erzeugt. Testen lässt sich das wie folgt:

curl -I -H'Origin: https://www.auxnet.de'  -XOPTIONS https://api.auxnet.de/
curl -I -H'Origin: https://auxnet.de'  -XOPTIONS https://api.auxnet.de/
curl -I -H'Origin: https://anderedomain.de'  -XOPTIONS https://api.auxnet.de/

Bei den ersten beiden Beispielen sollte ein korrekter Header für Access-Control-Allow-Origin erzeugt werden, in letztem Fall nicht.

Durch die Einloggen bei den Kommentaren werden zwei Cookies gesetzt. Mehr Informationen im Impressum.
Follow me