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 4a61b84c1c0 Strip CR/LF from user-supplied logical_date before stdlib 
logging (#67500)
4a61b84c1c0 is described below

commit 4a61b84c1c018fff572e7830755a2c24cea0f857
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue May 26 00:10:50 2026 +0200

    Strip CR/LF from user-supplied logical_date before stdlib logging (#67500)
    
    ``action_logging`` passed the raw ``logical_date`` query parameter into
    ``logger.exception("... %s", value)`` via Python's standard logging
    module on parse failure. On deployments configured with a non-JSON
    (plain-text) log formatter, an attacker could supply a value containing
    newline characters to forge fake log entries (CWE-117 log injection).
    
    The path is bounded — only exploitable on non-default plain-text
    formatters and only when the user actually triggers a parse failure —
    but the fix is cheap: replace ``\r`` and ``\n`` with spaces before
    formatting.
    
    Extract the sanitisation into ``_sanitize_for_stdlib_log()`` so the
    guard is testable in isolation. ``logger.exception`` is left in place
    on the stdlib logger (rather than swapped for ``structlog``) to keep
    the change minimal and avoid coupling the audit-log path's other
    behaviour changes into a security fix.
---
 .../src/airflow/api_fastapi/logging/decorators.py  | 17 +++++++-
 .../tests/unit/api_fastapi/logging/__init__.py     | 16 ++++++++
 .../unit/api_fastapi/logging/test_decorators.py    | 45 ++++++++++++++++++++++
 3 files changed, 77 insertions(+), 1 deletion(-)

diff --git a/airflow-core/src/airflow/api_fastapi/logging/decorators.py 
b/airflow-core/src/airflow/api_fastapi/logging/decorators.py
index a4734bb3e41..a48dc01a32e 100644
--- a/airflow-core/src/airflow/api_fastapi/logging/decorators.py
+++ b/airflow-core/src/airflow/api_fastapi/logging/decorators.py
@@ -33,6 +33,18 @@ from airflow.models import Log
 logger = logging.getLogger(__name__)
 
 
+def _sanitize_for_stdlib_log(value: str) -> str:
+    """
+    Strip CR/LF from a user-supplied value before passing it to stdlib's 
``%s``-style logging.
+
+    Defends against log injection when the deployment is configured with a 
non-JSON
+    (plain-text) log formatter: a newline in the value would otherwise let an 
attacker forge
+    log lines. ``structlog``-style formatters are unaffected, but the 
access-log path uses
+    the stdlib logger here, so the sanitisation is unconditional.
+    """
+    return value.replace("\r", " ").replace("\n", " ")
+
+
 def _mask_connection_fields(extra_fields):
     """Mask connection fields."""
     result = {}
@@ -163,7 +175,10 @@ def action_logging(event: str | None = None):
                         raise ParserError
                     log.logical_date = logical_date
                 except ParserError:
-                    logger.exception("Failed to parse logical_date from the 
request: %s", logical_date_value)
+                    logger.exception(
+                        "Failed to parse logical_date from the request: %s",
+                        _sanitize_for_stdlib_log(logical_date_value),
+                    )
             else:
                 logger.warning("Logical date is missing or empty")
         session.add(log)
diff --git a/airflow-core/tests/unit/api_fastapi/logging/__init__.py 
b/airflow-core/tests/unit/api_fastapi/logging/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/logging/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/airflow-core/tests/unit/api_fastapi/logging/test_decorators.py 
b/airflow-core/tests/unit/api_fastapi/logging/test_decorators.py
new file mode 100644
index 00000000000..67b1499df4c
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/logging/test_decorators.py
@@ -0,0 +1,45 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+
+from airflow.api_fastapi.logging.decorators import _sanitize_for_stdlib_log
+
+
+class TestSanitizeForStdlibLog:
+    """User input passed to stdlib ``%s``-style logging must have CR/LF 
stripped.
+
+    On a deployment configured with a non-JSON (plain-text) log formatter, a 
newline in the
+    value would otherwise let an attacker forge log lines (CWE-117). The audit 
logging path
+    in ``action_logging`` passes ``logical_date`` from the request through 
stdlib's ``logger``,
+    so this sanitisation is unconditional regardless of the formatter actually 
in use.
+    """
+
+    @pytest.mark.parametrize(
+        ("raw", "expected"),
+        [
+            ("2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z"),
+            ("bad\ndate", "bad date"),
+            ("bad\r\ndate", "bad  date"),
+            ("bad\rdate", "bad date"),
+            ("a\nb\nc", "a b c"),
+            ("", ""),
+        ],
+    )
+    def test_strips_cr_and_lf(self, raw, expected):
+        assert _sanitize_for_stdlib_log(raw) == expected

Reply via email to