This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 70257e69686 Reject wildcard origin in CORS config instead of toggling 
credentials (#67502)
70257e69686 is described below

commit 70257e6968603cdfcb5d34f03e01c5c001d2075f
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed May 27 23:57:15 2026 +0200

    Reject wildcard origin in CORS config instead of toggling credentials 
(#67502)
    
    The Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true
    combination is invalid per the CORS spec and browsers refuse to honour any
    response that does so. The previous fix (#66503) added an
    access_control_allow_credentials toggle, but allow_credentials=False would
    break Airflow's UI on any deployment where API and UI are on different
    origins, so that knob has no realistic use case.
    
    Drop the toggle, always send credentialed CORS, and fail loudly at startup
    with AirflowConfigException if access_control_allow_origins contains "*"
    so operators see the bad configuration immediately instead of debugging
    mysterious CORS errors in the browser.
    
    Closes #67193 (the revert is no longer needed once the underlying
    misconfiguration is rejected directly).
---
 airflow-core/docs/security/api.rst                 | 10 ++++---
 .../src/airflow/api_fastapi/core_api/app.py        | 19 ++++++++++--
 .../src/airflow/config_templates/config.yml        | 15 +++-------
 .../tests/unit/api_fastapi/core_api/test_app.py    | 35 ++++++++++++++--------
 4 files changed, 49 insertions(+), 30 deletions(-)

diff --git a/airflow-core/docs/security/api.rst 
b/airflow-core/docs/security/api.rst
index c02033d0b12..b45616ece89 100644
--- a/airflow-core/docs/security/api.rst
+++ b/airflow-core/docs/security/api.rst
@@ -86,10 +86,12 @@ from scripts running in the browser.
     access_control_allow_methods = POST, GET, OPTIONS, DELETE
     access_control_allow_origins = https://exampleclientapp1.com 
https://exampleclientapp2.com
 
-The ``Access-Control-Allow-Credentials`` header is included by default. Set
-``access_control_allow_credentials = False`` if you have configured
-``access_control_allow_origins`` and do not want browsers to send credentials
-(cookies, ``Authorization`` header) with cross-origin requests.
+Airflow's API always responds with ``Access-Control-Allow-Credentials: true`` 
so the UI and
+clients can send cookies and ``Authorization`` headers across origins. Because 
of that,
+``access_control_allow_origins`` must list the exact origins that need access 
— the wildcard
+``*`` is rejected at startup. The CORS spec forbids combining
+``Access-Control-Allow-Origin: *`` with credentialed responses, and browsers 
refuse any
+response that does so, so a wildcard origin would simply break every 
cross-origin request.
 
 Page size limit
 ---------------
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/app.py 
b/airflow-core/src/airflow/api_fastapi/core_api/app.py
index 20ecbedb236..27213a4c0a2 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/app.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py
@@ -29,7 +29,7 @@ from fastapi.staticfiles import StaticFiles
 from fastapi.templating import Jinja2Templates
 
 from airflow.api_fastapi.auth.tokens import get_signing_key
-from airflow.exceptions import AirflowException
+from airflow.exceptions import AirflowConfigException, AirflowException
 
 log = logging.getLogger(__name__)
 
@@ -143,13 +143,26 @@ def init_config(app: FastAPI) -> None:
     allow_origins = conf.getlist("api", "access_control_allow_origins")
     allow_methods = conf.getlist("api", "access_control_allow_methods")
     allow_headers = conf.getlist("api", "access_control_allow_headers")
-    allow_credentials = conf.getboolean("api", 
"access_control_allow_credentials", fallback=True)
+
+    if "*" in allow_origins:
+        # The CORS spec forbids combining `Access-Control-Allow-Origin: *` with
+        # `Access-Control-Allow-Credentials: true`, and browsers reject any 
response that does so
+        # (see https://fetch.spec.whatwg.org/#cors-protocol-and-credentials). 
Airflow's API needs
+        # credentialed requests for cookie / Authorization-header auth, so a 
wildcard origin is
+        # never a valid configuration. Fail loudly at startup instead of 
silently shipping a
+        # response shape that no browser will accept.
+        raise AirflowConfigException(
+            "`[api] access_control_allow_origins` must not contain `*`: the 
wildcard origin is "
+            "incompatible with the credentialed CORS Airflow's API requires, 
and browsers will "
+            "reject every cross-origin response. List the exact origins that 
need access "
+            "(e.g. `https://airflow.mycompany.com`) instead."
+        )
 
     if allow_origins or allow_methods or allow_headers:
         app.add_middleware(
             CORSMiddleware,
             allow_origins=allow_origins,
-            allow_credentials=allow_credentials,
+            allow_credentials=True,
             allow_methods=allow_methods,
             allow_headers=allow_headers,
         )
diff --git a/airflow-core/src/airflow/config_templates/config.yml 
b/airflow-core/src/airflow/config_templates/config.yml
index 4c650bbeb64..ae47f132b08 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -1821,21 +1821,14 @@ api:
     access_control_allow_origins:
       description: |
         Indicates whether the response can be shared with requesting code from 
the given origins.
-        Separate URLs with space.
+        Separate URLs with space. Wildcard (``*``) is not allowed: Airflow's 
API requires
+        credentialed CORS, which is incompatible with a wildcard origin per 
the CORS spec, and
+        browsers reject any response that combines the two. List exact origins 
instead
+        (for example ``https://airflow.mycompany.com``).
       type: string
       version_added: 2.2.0
       example: ~
       default: ""
-    access_control_allow_credentials:
-      description: |
-        Whether the FastAPI server includes the 
``Access-Control-Allow-Credentials`` header on
-        CORS responses. Defaults to True to preserve existing behavior; set to 
False if you have
-        configured ``access_control_allow_origins`` and do not want browsers 
to send credentials
-        (cookies, Authorization header) with cross-origin requests.
-      type: boolean
-      version_added: 3.2.2
-      example: ~
-      default: "True"
     grid_view_sorting_order:
       description: |
         Sorting order in grid view. Valid values are: ``topological``, 
``hierarchical_alphabetical``
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/test_app.py 
b/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
index 7a999e0f3b3..9e3c7219f03 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
@@ -149,20 +149,31 @@ class TestRouterLevelDefaultDeny:
         )
 
 
-class TestCorsMiddlewareAllowCredentials:
-    @pytest.mark.parametrize(
-        ("config_value", "expected_allow_credentials"),
-        [(None, True), ("True", True), ("False", False)],
-    )
-    def test_init_config_passes_allow_credentials(self, config_value, 
expected_allow_credentials):
-        config = {("api", "access_control_allow_origins"): 
"https://example.com"}
-        if config_value is not None:
-            config[("api", "access_control_allow_credentials")] = config_value
-
-        with conf_vars(config):
+class TestCorsMiddlewareConfig:
+    def test_init_config_enables_credentialed_cors_for_explicit_origins(self):
+        with conf_vars({("api", "access_control_allow_origins"): 
"https://example.com"}):
             app = FastAPI()
             init_config(app)
 
         cors_middlewares = [m for m in app.user_middleware if m.cls is 
CORSMiddleware]
         assert len(cors_middlewares) == 1
-        assert cors_middlewares[0].kwargs["allow_credentials"] is 
expected_allow_credentials
+        assert cors_middlewares[0].kwargs["allow_credentials"] is True
+        assert cors_middlewares[0].kwargs["allow_origins"] == 
["https://example.com";]
+
+    @pytest.mark.parametrize(
+        "origins",
+        ["*", "https://example.com,*";, "*,https://example.com";],
+    )
+    def test_init_config_rejects_wildcard_origin(self, origins):
+        """Wildcard origin is incompatible with credentialed CORS; reject it 
at startup.
+
+        Browsers refuse any response that combines 
``Access-Control-Allow-Origin: *`` with
+        ``Access-Control-Allow-Credentials: true``, so silently accepting 
``*`` would just ship
+        a configuration where every cross-origin request fails. Fail loudly 
instead.
+        """
+        from airflow.exceptions import AirflowConfigException
+
+        with conf_vars({("api", "access_control_allow_origins"): origins}):
+            app = FastAPI()
+            with pytest.raises(AirflowConfigException, match=r"must not 
contain `\*`"):
+                init_config(app)

Reply via email to