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 c0ae2127e17 Fix passphrase quoting in decrypt_remote_file_to_string 
for Windows (#69908)
c0ae2127e17 is described below

commit c0ae2127e174205b8faa43f25b5c912c09353bd1
Author: Dan Kalenga <[email protected]>
AuthorDate: Sat Aug 29 19:26:11 2026 +0200

    Fix passphrase quoting in decrypt_remote_file_to_string for Windows (#69908)
    
    * Fix passphrase quoting in decrypt_remote_file_to_string for Windows 
remotes
    
    * Restored shlex.quote on remote path and drop misleading password = None
    
    ---------
    
    Co-authored-by: kada2004 <dankalenga3@gmail,com>
---
 .../providers/teradata/utils/encryption_utils.py   | 25 +++----------
 .../unit/teradata/utils/test_encryption_utils.py   | 43 ++++++++++++----------
 2 files changed, 30 insertions(+), 38 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 30cd0340f10..bce85c84821 100644
--- 
a/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py
+++ 
b/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py
@@ -24,17 +24,12 @@ import subprocess
 
 
 def generate_random_password(length=12):
-    # Define the character set: letters, digits, and special characters
     characters = string.ascii_letters + string.digits + string.punctuation
-    # Generate a random password
     password = "".join(secrets.choice(characters) for _ in range(length))
     return password
 
 
 def generate_encrypted_file_with_openssl(file_path: str, password: str, 
out_file: str):
-    # Write plaintext temporarily to file
-
-    # Run openssl enc with AES-256-CBC, pbkdf2, salt
     cmd = [
         "openssl",
         "enc",
@@ -54,25 +49,17 @@ def generate_encrypted_file_with_openssl(file_path: str, 
password: str, out_file
 
 
 def decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, 
bteq_command_str):
-    # Run openssl decrypt command on remote machine
-    quoted_password = shell_quote_single(password)
-
+    # Use -pass stdin to avoid shell quoting on any OS and keep the passphrase
+    # out of the remote process table where ps could expose it.
     decrypt_cmd = (
-        f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass 
pass:{quoted_password} -in {shlex.quote(remote_enc_file)} | "
+        f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass stdin -in 
{shlex.quote(remote_enc_file)} | "
         + bteq_command_str
     )
-    # Clear password to prevent lingering sensitive data
-    password = None
-    quoted_password = None
     stdin, stdout, stderr = ssh_client.exec_command(decrypt_cmd)
-    # Wait for command to finish
+    stdin.write(password + "\n")
+    stdin.flush()
+    stdin.channel.shutdown_write()
     exit_status = stdout.channel.recv_exit_status()
     output = stdout.read().decode()
     err = stderr.read().decode()
     return exit_status, output, err
-
-
-def shell_quote_single(s):
-    # Escape single quotes in s, then wrap in single quotes
-    # In shell, to include a single quote inside single quotes, close, add 
'\'' and reopen
-    return "'" + s.replace("'", "'\\''") + "'"
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 145074d304e..eee17c6fa51 100644
--- a/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py
+++ b/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py
@@ -23,7 +23,6 @@ from airflow.providers.teradata.utils.encryption_utils import 
(
     decrypt_remote_file_to_string,
     generate_encrypted_file_with_openssl,
     generate_random_password,
-    shell_quote_single,
 )
 
 
@@ -31,9 +30,8 @@ class TestEncryptionUtils:
     def test_generate_random_password_length(self):
         pwd = generate_random_password(16)
         assert len(pwd) == 16
-        # Check characters are in allowed set
         allowed_chars = string.ascii_letters + string.digits + 
string.punctuation
-        assert (all(c in allowed_chars for c in pwd)) is True
+        assert all(c in allowed_chars for c in pwd) is True
 
     @patch("subprocess.run")
     def test_generate_encrypted_file_with_openssl_calls_subprocess(self, 
mock_run):
@@ -72,16 +70,6 @@ class TestEncryptionUtils:
         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)
-        assert quoted == "'simple'"
-
-    def test_shell_quote_single_with_single_quote(self):
-        s = "O'Reilly"
-        quoted = shell_quote_single(s)
-        assert quoted == "'O'\\''Reilly'"
-
     def test_decrypt_remote_file_to_string(self):
         password = "mysecret"
         remote_enc_file = "/remote/encrypted.enc"
@@ -91,25 +79,42 @@ class TestEncryptionUtils:
         mock_stdin = MagicMock()
         mock_stdout = MagicMock()
         mock_stderr = MagicMock()
-
-        # Setup mock outputs and exit code
         mock_stdout.channel.recv_exit_status.return_value = 0
         mock_stdout.read.return_value = b"decrypted output"
         mock_stderr.read.return_value = b""
-
         ssh_client.exec_command.return_value = (mock_stdin, mock_stdout, 
mock_stderr)
 
         exit_status, output, err = decrypt_remote_file_to_string(
             ssh_client, remote_enc_file, password, bteq_command_str
         )
 
-        quoted_password = shell_quote_single(password)
         expected_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 stdin -in 
{remote_enc_file} | "
             + bteq_command_str
         )
-
         ssh_client.exec_command.assert_called_once_with(expected_cmd)
+        mock_stdin.write.assert_called_once_with(password + "\n")
+        mock_stdin.flush.assert_called_once()
+        mock_stdin.channel.shutdown_write.assert_called_once()
         assert exit_status == 0
         assert output == "decrypted output"
         assert err == ""
+
+    def test_decrypt_remote_file_passphrase_not_on_argv(self):
+        """The passphrase is passed via stdin, never on the remote command 
line."""
+        password = "s3cr3t&rm -rf ~"
+        remote_enc_file = "/remote/encrypted.enc"
+        ssh_client = MagicMock()
+        mock_stdin = MagicMock()
+        mock_stdout = MagicMock()
+        mock_stderr = MagicMock()
+        mock_stdout.channel.recv_exit_status.return_value = 0
+        mock_stdout.read.return_value = b""
+        mock_stderr.read.return_value = b""
+        ssh_client.exec_command.return_value = (mock_stdin, mock_stdout, 
mock_stderr)
+
+        decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, 
"bteq")
+
+        cmd = ssh_client.exec_command.call_args[0][0]
+        assert password not in cmd, "passphrase leaked onto remote command 
line"
+        assert "-pass stdin" in cmd

Reply via email to