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