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 b703da5326d Quote file paths in Teradata TPT shell commands and pass 
the openssl passphrase via stdin (#69380)
b703da5326d is described below

commit b703da5326d43a53011442a59e23715b757ab7f4
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed Jul 22 19:50:10 2026 +0200

    Quote file paths in Teradata TPT shell commands and pass the openssl 
passphrase via stdin (#69380)
    
    The Teradata provider's tpt_util.py and encryption_utils.py build several 
shell
    commands (run locally or over SSH) that interpolate file paths directly, and
    pass the openssl passphrase on the command line.
    
    Quote the interpolated file paths with shlex.quote() in the POSIX shell 
commands
    (shred, the dd/rm fallback, chmod, which, and the openssl -in/-out paths) so
    paths containing spaces or shell metacharacters are handled correctly, and 
feed
    the openssl passphrase on stdin (-pass stdin) for the local
    generate_encrypted_file_with_openssl so it is not exposed in the local 
host's
    process list.
    
    Windows command branches and the remote SSH openssl -pass pass: path are 
left
    unchanged.
    
    Generated-by: Claude Opus 4.8 (1M context) following the guidelines at
    
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
---
 .../providers/teradata/utils/encryption_utils.py   |  9 ++++---
 .../airflow/providers/teradata/utils/tpt_util.py   | 16 +++++++------
 .../unit/teradata/utils/test_encryption_utils.py   | 14 ++++++++++-
 .../tests/unit/teradata/utils/test_tpt_util.py     | 28 ++++++++++++++++++++++
 4 files changed, 56 insertions(+), 11 deletions(-)

diff --git 
a/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py 
b/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py
index 57ed4b98558..30cd0340f10 100644
--- 
a/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py
+++ 
b/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py
@@ -18,6 +18,7 @@
 from __future__ import annotations
 
 import secrets
+import shlex
 import string
 import subprocess
 
@@ -41,13 +42,15 @@ def generate_encrypted_file_with_openssl(file_path: str, 
password: str, out_file
         "-salt",
         "-pbkdf2",
         "-pass",
-        f"pass:{password}",
+        "stdin",
         "-in",
         file_path,
         "-out",
         out_file,
     ]
-    subprocess.run(cmd, check=True)
+    # Pass the passphrase on stdin rather than the command line so it is not
+    # visible in the local host's process list (ps).
+    subprocess.run(cmd, input=f"{password}\n".encode(), check=True)
 
 
 def decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, 
bteq_command_str):
@@ -55,7 +58,7 @@ def decrypt_remote_file_to_string(ssh_client, 
remote_enc_file, password, bteq_co
     quoted_password = shell_quote_single(password)
 
     decrypt_cmd = (
-        f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass 
pass:{quoted_password} -in {remote_enc_file} | "
+        f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass 
pass:{quoted_password} -in {shlex.quote(remote_enc_file)} | "
         + bteq_command_str
     )
     # Clear password to prevent lingering sensitive data
diff --git 
a/providers/teradata/src/airflow/providers/teradata/utils/tpt_util.py 
b/providers/teradata/src/airflow/providers/teradata/utils/tpt_util.py
index 26678b15ca9..1caaf7ef445 100644
--- a/providers/teradata/src/airflow/providers/teradata/utils/tpt_util.py
+++ b/providers/teradata/src/airflow/providers/teradata/utils/tpt_util.py
@@ -18,6 +18,7 @@ from __future__ import annotations
 
 import logging
 import os
+import shlex
 import shutil
 import stat
 import subprocess
@@ -123,14 +124,15 @@ def remote_secure_delete(
                     )
                 elif shred_available:
                     # UNIX/Linux with shred
-                    execute_remote_command(ssh_client, f"shred --remove 
{file_path}")
+                    execute_remote_command(ssh_client, f"shred --remove 
{shlex.quote(file_path)}")
                 else:
                     # UNIX/Linux without shred - overwrite then delete
                     execute_remote_command(
                         ssh_client,
-                        f"if [ -f {file_path} ]; then "
-                        f"dd if=/dev/zero of={file_path} bs=4096 
count=$(($(stat -c '%s' {file_path})/4096+1)) 2>/dev/null; "
-                        f"rm -f {file_path}; fi",
+                        f"if [ -f {shlex.quote(file_path)} ]; then "
+                        f"dd if=/dev/zero of={shlex.quote(file_path)} bs=4096 "
+                        f"count=$(($(stat -c '%s' 
{shlex.quote(file_path)})/4096+1)) 2>/dev/null; "
+                        f"rm -f {shlex.quote(file_path)}; fi",
                     )
             except Exception as e:
                 logger.warning("Failed to process remote file %s: %s", 
file_path, str(e))
@@ -246,7 +248,7 @@ def _set_windows_file_permissions(
 
 def _set_unix_file_permissions(ssh_client: SSHClient, remote_file_path: str, 
logger: logging.Logger) -> None:
     """Set read-only permissions on Unix/Linux remote file."""
-    command = f"chmod 400 {remote_file_path}"
+    command = f"chmod 400 {shlex.quote(remote_file_path)}"
 
     exit_status, stdout_data, stderr_data = execute_remote_command(ssh_client, 
command)
 
@@ -355,7 +357,7 @@ def verify_tpt_utility_on_remote_host(
         if remote_os == "windows":
             command = f"where {utility}"
         else:
-            command = f"which {utility}"
+            command = f"which {shlex.quote(utility)}"
 
         exit_status, output, error = execute_remote_command(ssh_client, 
command)
 
@@ -622,7 +624,7 @@ def decrypt_remote_file(
         password_escaped = password.replace("'", "'\\''")  # Escape single 
quotes
         decrypt_cmd = (
             f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass 
pass:'{password_escaped}' "
-            f"-in {remote_enc_file} -out {remote_dec_file}"
+            f"-in {shlex.quote(remote_enc_file)} -out 
{shlex.quote(remote_dec_file)}"
         )
 
     exit_status, stdout_data, stderr_data = execute_remote_command(ssh_client, 
decrypt_cmd)
diff --git 
a/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py 
b/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py
index fdf1c8bc21b..145074d304e 100644
--- a/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py
+++ b/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py
@@ -51,15 +51,27 @@ class TestEncryptionUtils:
                 "-salt",
                 "-pbkdf2",
                 "-pass",
-                f"pass:{password}",
+                "stdin",
                 "-in",
                 file_path,
                 "-out",
                 out_file,
             ],
+            input=f"{password}\n".encode(),
             check=True,
         )
 
+    @patch("subprocess.run")
+    def test_generate_encrypted_file_passphrase_not_on_argv(self, mock_run):
+        """The passphrase is fed on stdin, never placed on the command line 
(ps-visible)."""
+        password = "s3cr3t;rm -rf ~"
+        generate_encrypted_file_with_openssl("/tmp/plain.txt", password, 
"/tmp/out.enc")
+        args, kwargs = mock_run.call_args
+        cmd = args[0]
+        assert "stdin" in cmd
+        assert not any(password in str(part) for part in cmd), "passphrase 
leaked onto argv"
+        assert kwargs["input"] == f"{password}\n".encode()
+
     def test_shell_quote_single_simple(self):
         s = "simple"
         quoted = shell_quote_single(s)
diff --git a/providers/teradata/tests/unit/teradata/utils/test_tpt_util.py 
b/providers/teradata/tests/unit/teradata/utils/test_tpt_util.py
index e1e2fe1437b..05c52953826 100644
--- a/providers/teradata/tests/unit/teradata/utils/test_tpt_util.py
+++ b/providers/teradata/tests/unit/teradata/utils/test_tpt_util.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import os
+import shlex
 import stat
 import subprocess
 import tempfile
@@ -164,6 +165,19 @@ class TestTptUtil:
         assert mock_execute_cmd.call_args_list == expected_calls
         mock_logger.info.assert_called_with("Processed remote files: %s", 
"/remote/file1, /remote/file2")
 
+    @patch("airflow.providers.teradata.utils.tpt_util.get_remote_os")
+    @patch("airflow.providers.teradata.utils.tpt_util.execute_remote_command")
+    def test_remote_secure_delete_quotes_metacharacter_path(self, 
mock_execute_cmd, mock_get_remote_os):
+        """A remote path with spaces / shell metacharacters is shell-quoted in 
the shred command."""
+        mock_ssh = Mock()
+        mock_logger = Mock()
+        mock_get_remote_os.return_value = "unix"
+        mock_execute_cmd.side_effect = [(0, "/usr/bin/shred", ""), (0, "", "")]
+        evil = "/remote/a b; rm -rf ~"
+        remote_secure_delete(mock_ssh, [evil], mock_logger)
+        cmd = mock_execute_cmd.call_args_list[1].args[1]
+        assert cmd == f"shred --remove {shlex.quote(evil)}"
+
     @patch("airflow.providers.teradata.utils.tpt_util.get_remote_os")
     @patch("airflow.providers.teradata.utils.tpt_util.execute_remote_command")
     def test_remote_secure_delete_without_shred(self, mock_execute_cmd, 
mock_get_remote_os):
@@ -570,6 +584,20 @@ class TestTptUtil:
         mock_get_remote_os.assert_called_once_with(mock_ssh, mock_logger)
         mock_execute_cmd.assert_called_once_with(mock_ssh, "chmod 400 
/remote/file")
 
+    @patch("airflow.providers.teradata.utils.tpt_util.get_remote_os")
+    @patch("airflow.providers.teradata.utils.tpt_util.execute_remote_command")
+    def test_set_remote_file_permissions_unix_quotes_metacharacter_path(
+        self, mock_execute_cmd, mock_get_remote_os
+    ):
+        """The chmod command shell-quotes a path containing spaces / 
metacharacters."""
+        mock_ssh = Mock()
+        mock_logger = Mock()
+        mock_get_remote_os.return_value = "unix"
+        mock_execute_cmd.return_value = (0, "", "")
+        evil = "/remote/a b; touch pwned"
+        set_remote_file_permissions(mock_ssh, evil, mock_logger)
+        mock_execute_cmd.assert_called_once_with(mock_ssh, f"chmod 400 
{shlex.quote(evil)}")
+
     @patch("airflow.providers.teradata.utils.tpt_util.get_remote_os")
     @patch("airflow.providers.teradata.utils.tpt_util.execute_remote_command")
     def test_set_remote_file_permissions_windows(self, mock_execute_cmd, 
mock_get_remote_os):

Reply via email to