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"
+        )

Reply via email to