* `allow_methods` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use `['*']` to allow all standard methods.
* `allow_headers` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to `[]`. You can use `['*']` to allow all headers. The `Accept`, `Accept-Language`, `Content-Language` and `Content-Type` headers are always allowed for CORS requests.
* `allow_credentials` - Indicate that cookies should be supported for cross-origin requests. Defaults to `False`. Also, `allow_origins`, `allow_methods` and `allow_headers` cannot be set to `['*']` for credentials to be allowed, all of them must be explicitly specified.
+* `allow_private_network` - Indicates whether to accept cross-origin requests over a private network. Defaults to `False`.
* `expose_headers` - Indicate any response headers that should be made accessible to the browser. Defaults to `[]`.
* `max_age` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to `600`.
Any request with an `Origin` header. In this case the middleware will pass the
request through as normal, but will include appropriate CORS headers on the response.
+#### Private Network Access (PNA)
+
+Private Network Access is a browser security feature that restricts websites from public networks from accessing servers on private networks.
+
+When a website attempts to make such a cross-network request, the browser will send a `Access-Control-Request-Private-Network: true` header in the
+pre-flight request. If the `allow_private_network` flag is set to `True`, the middleware will include the `Access-Control-Allow-Private-Network: true`
+header in the response, allowing the request. If set to `False`, the middleware will return a 400 response, blocking the request.
+
### CORSMiddleware Global Enforcement
When using CORSMiddleware with your Starlette application, it's important to ensure that CORS headers are applied even to error responses generated by unhandled exceptions. The recommended solution is to wrap the entire Starlette application with CORSMiddleware. This approach guarantees that even if an exception is caught by ServerErrorMiddleware (or other outer error-handling middleware), the response will still include the proper `Access-Control-Allow-Origin` header.
allow_headers: Sequence[str] = (),
allow_credentials: bool = False,
allow_origin_regex: str | None = None,
+ allow_private_network: bool = False,
expose_headers: Sequence[str] = (),
max_age: int = 600,
) -> None:
allow_all_headers = "*" in allow_headers
preflight_explicit_allow_origin = not allow_all_origins or allow_credentials
- simple_headers = {}
+ simple_headers: dict[str, str] = {}
if allow_all_origins:
simple_headers["Access-Control-Allow-Origin"] = "*"
if allow_credentials:
if expose_headers:
simple_headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers)
- preflight_headers = {}
+ preflight_headers: dict[str, str] = {}
if preflight_explicit_allow_origin:
# The origin value will be set in preflight_response() if it is allowed.
preflight_headers["Vary"] = "Origin"
self.allow_all_headers = allow_all_headers
self.preflight_explicit_allow_origin = preflight_explicit_allow_origin
self.allow_origin_regex = compiled_allow_origin_regex
+ self.allow_private_network = allow_private_network
self.simple_headers = simple_headers
self.preflight_headers = preflight_headers
requested_origin = request_headers["origin"]
requested_method = request_headers["access-control-request-method"]
requested_headers = request_headers.get("access-control-request-headers")
+ requested_private_network = request_headers.get("access-control-request-private-network")
headers = dict(self.preflight_headers)
- failures = []
+ failures: list[str] = []
if self.is_allowed_origin(origin=requested_origin):
if self.preflight_explicit_allow_origin:
failures.append("headers")
break
+ if requested_private_network is not None:
+ if self.allow_private_network:
+ headers["Access-Control-Allow-Private-Network"] = "true"
+ else:
+ failures.append("private-network")
+
# We don't strictly need to use 400 responses here, since its up to
# the browser to enforce the CORS policy, but its more informative
# if we do.
assert response.headers["access-control-allow-origin"] == "https://subdomain.example.org"
assert "access-control-allow-credentials" not in response.headers
- # Test diallowed standard response
+ # Test disallowed standard response
headers = {"Origin": "https://subdomain.example.org.hacker.com"}
response = client.get("/", headers=headers)
assert response.status_code == 200
response = client.get("/", headers={"Origin": "https://someplace.org"})
assert response.headers["access-control-allow-origin"] == "*"
assert "access-control-allow-credentials" not in response.headers
+
+
+def test_cors_private_network_access_allowed(test_client_factory: TestClientFactory) -> None:
+ def homepage(request: Request) -> PlainTextResponse:
+ return PlainTextResponse("Homepage", status_code=200)
+
+ app = Starlette(
+ routes=[Route("/", endpoint=homepage)],
+ middleware=[
+ Middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_private_network=True,
+ )
+ ],
+ )
+
+ client = test_client_factory(app)
+
+ headers_without_pna = {"Origin": "https://example.org", "Access-Control-Request-Method": "GET"}
+ headers_with_pna = {**headers_without_pna, "Access-Control-Request-Private-Network": "true"}
+
+ # Test preflight with Private Network Access request
+ response = client.options("/", headers=headers_with_pna)
+ assert response.status_code == 200
+ assert response.text == "OK"
+ assert response.headers["access-control-allow-private-network"] == "true"
+
+ # Test preflight without Private Network Access request
+ response = client.options("/", headers=headers_without_pna)
+ assert response.status_code == 200
+ assert response.text == "OK"
+ assert "access-control-allow-private-network" not in response.headers
+
+ # The access-control-allow-private-network header is not set for non-preflight requests
+ response = client.get("/", headers=headers_with_pna)
+ assert response.status_code == 200
+ assert response.text == "Homepage"
+ assert "access-control-allow-private-network" not in response.headers
+ assert "access-control-allow-origin" in response.headers
+
+
+def test_cors_private_network_access_disallowed(test_client_factory: TestClientFactory) -> None:
+ def homepage(request: Request) -> None: ... # pragma: no cover
+
+ app = Starlette(
+ routes=[Route("/", endpoint=homepage)],
+ middleware=[
+ Middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_private_network=False,
+ )
+ ],
+ )
+
+ client = test_client_factory(app)
+
+ # Test preflight with Private Network Access request when not allowed
+ headers_without_pna = {"Origin": "https://example.org", "Access-Control-Request-Method": "GET"}
+ headers_with_pna = {**headers_without_pna, "Access-Control-Request-Private-Network": "true"}
+
+ response = client.options("/", headers=headers_without_pna)
+ assert response.status_code == 200
+ assert response.text == "OK"
+ assert "access-control-allow-private-network" not in response.headers
+
+ # If the request includes a Private Network Access header, but the middleware is configured to disallow it, the
+ # request should be denied with a 400 response.
+ response = client.options("/", headers=headers_with_pna)
+ assert response.status_code == 400
+ assert response.text == "Disallowed CORS private-network"
+ assert "access-control-allow-private-network" not in response.headers