This is an automated email from the ASF dual-hosted git repository.
shahar1 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 407485ef8cb Escape values interpolated into Google Drive q= queries
(#72166)
407485ef8cb is described below
commit 407485ef8cb870e1ca244d782abe2fd85c195dc2
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sun Sep 20 11:44:12 2026 +0200
Escape values interpolated into Google Drive q= queries (#72166)
GoogleDriveHook builds Drive search expressions by interpolating names
directly
into single-quoted string literals: _ensure_folders_exists composes
name='<folder>' and '<parent>' in parents, and get_file_id composes
name = '<file_name>' and parents in '<folder_id>'.
The Drive query language delimits string literals with single quotes and
escapes
' and \ with a backslash, but neither call site escaped either character. A
name
containing a quote therefore produced a malformed expression, and the
resulting
search did not match what the caller asked for. Names routinely arrive from
a
bucket listing rather than being written by hand, so quotes in them are
ordinary
input rather than a special case.
Add _escape_drive_query_value and route all four interpolations through it.
Backslashes are escaped before quotes so the backslash introduced by the
quote
escape is not doubled.
---
.../airflow/providers/google/suite/hooks/drive.py | 21 +++++--
.../tests/unit/google/suite/hooks/test_drive.py | 70 +++++++++++++++++++++-
2 files changed, 86 insertions(+), 5 deletions(-)
diff --git a/providers/google/src/airflow/providers/google/suite/hooks/drive.py
b/providers/google/src/airflow/providers/google/suite/hooks/drive.py
index 7888fb1d119..abb8bc9cf0a 100644
--- a/providers/google/src/airflow/providers/google/suite/hooks/drive.py
+++ b/providers/google/src/airflow/providers/google/suite/hooks/drive.py
@@ -29,6 +29,19 @@ from googleapiclient.http import HttpRequest, MediaFileUpload
from airflow.providers.google.common.hooks.base_google import GoogleBaseHook
+def _escape_drive_query_value(value: str) -> str:
+ r"""
+ Escape a value for interpolation into a Drive ``q=`` string literal.
+
+ The Drive query language delimits string literals with single quotes and
escapes
+ ``'`` and ``\\`` with a backslash. Values reaching these queries are
frequently
+ object names taken from a source bucket listing rather than written by
hand, so a
+ quote in the value is ordinary input; left unescaped it ends the literal
early and
+ the expression no longer means what the caller intended.
+ """
+ return str(value).replace("\\", "\\\\").replace("'", "\\'")
+
+
class GoogleDriveHook(GoogleBaseHook):
"""
Hook for the Google Drive APIs.
@@ -87,8 +100,8 @@ class GoogleDriveHook(GoogleBaseHook):
conditions = [
"trashed=false",
"mimeType='application/vnd.google-apps.folder'",
- f"name='{current_folder}'",
- f"'{current_parent}' in parents",
+ f"name='{_escape_drive_query_value(current_folder)}'",
+ f"'{_escape_drive_query_value(current_parent)}' in parents",
]
result = (
service.files()
@@ -223,9 +236,9 @@ class GoogleDriveHook(GoogleBaseHook):
:return: Google Drive file id if the file exists, otherwise None
"""
- query = f"name = '{file_name}'"
+ query = f"name = '{_escape_drive_query_value(file_name)}'"
if folder_id:
- query += f" and '{folder_id}' in parents"
+ query += f" and '{_escape_drive_query_value(folder_id)}' in
parents"
if not include_trashed:
query += " and trashed=false"
diff --git a/providers/google/tests/unit/google/suite/hooks/test_drive.py
b/providers/google/tests/unit/google/suite/hooks/test_drive.py
index 85ed3d622c0..0c3894712c5 100644
--- a/providers/google/tests/unit/google/suite/hooks/test_drive.py
+++ b/providers/google/tests/unit/google/suite/hooks/test_drive.py
@@ -21,7 +21,7 @@ from unittest import mock
import pytest
-from airflow.providers.google.suite.hooks.drive import GoogleDriveHook
+from airflow.providers.google.suite.hooks.drive import GoogleDriveHook,
_escape_drive_query_value
from unit.google.cloud.utils.base_gcp_mock import
GCP_CONNECTION_WITH_PROJECT_ID
@@ -523,3 +523,71 @@ class TestGoogleDriveHook:
)
mock_execute.assert_called_once()
assert result == {"id": "NEW_FILE_ID", "webViewLink":
"https://example.com/view"}
+
+
[email protected](
+ ("raw", "expected"),
+ [
+ ("plain.csv", "plain.csv"),
+ ("o'brien.csv", "o\\'brien.csv"),
+ ("back\\slash", "back\\\\slash"),
+ ("x' or name!='", "x\\' or name!=\\'"),
+ ],
+)
+def test_escape_drive_query_value(raw, expected):
+ """Quotes and backslashes are escaped; backslashes first so the
quote-escape is not doubled."""
+ assert _escape_drive_query_value(raw) == expected
+
+
[email protected]_test
+class TestDriveQueryQuoting:
+ """Names containing quotes are carried through the ``q=`` expression
intact.
+
+ Names routinely come from a bucket listing rather than being written by
hand, so a
+ quote in one is ordinary input and must not change how the expression
parses.
+ """
+
+ def setup_method(self):
+ self.patcher_get_connection = mock.patch(
+ f"{BASEHOOK_PATCH_PATH}.get_connection",
return_value=GCP_CONNECTION_WITH_PROJECT_ID
+ )
+ self.patcher_get_connection.start()
+ self.gdrive_hook = GoogleDriveHook(gcp_conn_id="test")
+
+ def teardown_method(self) -> None:
+ self.patcher_get_connection.stop()
+
+
@mock.patch("airflow.providers.google.suite.hooks.drive.GoogleDriveHook.get_conn")
+ def test_get_file_id_escapes_quote_in_file_name(self, mock_get_conn):
+ mock_list = mock_get_conn.return_value.files.return_value.list
+ mock_list.return_value.execute.side_effect = [{"files": []}]
+
+ self.gdrive_hook.get_file_id("folder1", "evil' or name!='")
+
+ query = mock_list.call_args.kwargs["q"]
+ assert query == r"name = 'evil\' or name!=\'' and 'folder1' in parents"
+
+
@mock.patch("airflow.providers.google.suite.hooks.drive.GoogleDriveHook.get_conn")
+ def test_get_file_id_escapes_quote_in_folder_id(self, mock_get_conn):
+ mock_list = mock_get_conn.return_value.files.return_value.list
+ mock_list.return_value.execute.side_effect = [{"files": []}]
+
+ self.gdrive_hook.get_file_id("evil' or name!='", "file1")
+
+ query = mock_list.call_args.kwargs["q"]
+ assert query == r"name = 'file1' and 'evil\' or name!=\'' in parents"
+
+
@mock.patch("airflow.providers.google.suite.hooks.drive.GoogleDriveHook.get_conn")
+ def test_ensure_folders_exists_escapes_quote_in_folder_name(self,
mock_get_conn):
+ mock_list = mock_get_conn.return_value.files.return_value.list
+ mock_list.return_value.execute.side_effect = [
+ {"files": [{"id": "ID_1", "name": "x"}]},
+ ]
+
+ self.gdrive_hook._ensure_folders_exists("evil' or name!='", "root")
+
+ query = mock_list.call_args.kwargs["q"]
+ assert query == (
+ "trashed=false and mimeType='application/vnd.google-apps.folder' "
+ r"and name='evil\' or name!=\'' and 'root' in parents"
+ )