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):