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