From e8b20e0981ba6372674ee71ded2be681830ef1a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:20:00 +0000 Subject: [PATCH 1/8] Initial plan From bbdf19b6b4970757352213219c44b7f2c86d5e34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:34:43 +0000 Subject: [PATCH 2/8] Fix issue #1996: Remove sensitive query parameters from debug logging - Updated all backend client files to log only SQL queries without parameter values - Changed logging from `self.log.debug("%s: %s", query, values)` to `self.log.debug("%s", query)` - Affects 7 backend clients: MySQL, SQLite, PostgreSQL (psycopg), ODBC, Oracle, MSSQL, AsyncPG - Prevents exposure of sensitive data like passwords, tokens, etc. in debug logs - Maintains debugging capability by still logging the SQL query structure Co-authored-by: bddap <2702854+bddap@users.noreply.github.com> --- tortoise/backends/asyncpg/client.py | 10 +++++----- tortoise/backends/mssql/client.py | 2 +- tortoise/backends/mysql/client.py | 8 ++++---- tortoise/backends/odbc/client.py | 6 +++--- tortoise/backends/oracle/client.py | 2 +- tortoise/backends/psycopg/client.py | 4 ++-- tortoise/backends/sqlite/client.py | 10 +++++----- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tortoise/backends/asyncpg/client.py b/tortoise/backends/asyncpg/client.py index 1bd31cba2..f461870f7 100644 --- a/tortoise/backends/asyncpg/client.py +++ b/tortoise/backends/asyncpg/client.py @@ -107,14 +107,14 @@ def _in_transaction(self) -> TransactionContext: @translate_exceptions async def execute_insert(self, query: str, values: list) -> asyncpg.Record | None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) # TODO: Cache prepared statement return await connection.fetchrow(query, *values) @translate_exceptions async def execute_many(self, query: str, values: list) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) # TODO: Consider using copy_records_to_table instead transaction = connection.transaction() await transaction.start() @@ -129,7 +129,7 @@ async def execute_many(self, query: str, values: list) -> None: @translate_exceptions async def execute_query(self, query: str, values: list | None = None) -> tuple[int, list[dict]]: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) if values: params = [query, *values] else: @@ -148,7 +148,7 @@ async def execute_query(self, query: str, values: list | None = None) -> tuple[i @translate_exceptions async def execute_query_dict(self, query: str, values: list | None = None) -> list[dict]: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) if values: return list(map(dict, await connection.fetch(query, *values))) return list(map(dict, await connection.fetch(query))) @@ -180,7 +180,7 @@ def acquire_connection(self) -> ConnectionWrapper[asyncpg.Connection]: @translate_exceptions async def execute_many(self, query: str, values: list) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) # TODO: Consider using copy_records_to_table instead await connection.executemany(query, values) diff --git a/tortoise/backends/mssql/client.py b/tortoise/backends/mssql/client.py index bed3f869e..0d2e4660a 100644 --- a/tortoise/backends/mssql/client.py +++ b/tortoise/backends/mssql/client.py @@ -51,7 +51,7 @@ def _in_transaction(self) -> TransactionContext: @translate_exceptions async def execute_insert(self, query: str, values: list) -> int: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) async with connection.cursor() as cursor: await cursor.execute(query, values) await cursor.execute("SELECT @@IDENTITY;") diff --git a/tortoise/backends/mysql/client.py b/tortoise/backends/mysql/client.py index c2e0886c6..88aac52c8 100644 --- a/tortoise/backends/mysql/client.py +++ b/tortoise/backends/mysql/client.py @@ -179,7 +179,7 @@ def _in_transaction(self) -> TransactionContext: @translate_exceptions async def execute_insert(self, query: str, values: list) -> int: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) async with connection.cursor() as cursor: await cursor.execute(query, values) return cursor.lastrowid # return auto-generated id @@ -187,7 +187,7 @@ async def execute_insert(self, query: str, values: list) -> int: @translate_exceptions async def execute_many(self, query: str, values: list) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) async with connection.cursor() as cursor: if self.capabilities.supports_transactions: await connection.begin() @@ -204,7 +204,7 @@ async def execute_many(self, query: str, values: list) -> None: @translate_exceptions async def execute_query(self, query: str, values: list | None = None) -> tuple[int, list[dict]]: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) async with connection.cursor() as cursor: await cursor.execute(query, values) rows = await cursor.fetchall() @@ -244,7 +244,7 @@ def acquire_connection(self) -> ConnectionWrapper[mysql.Connection]: @translate_exceptions async def execute_many(self, query: str, values: list) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) async with connection.cursor() as cursor: await cursor.executemany(query, values) diff --git a/tortoise/backends/odbc/client.py b/tortoise/backends/odbc/client.py index 91a436948..97b991001 100644 --- a/tortoise/backends/odbc/client.py +++ b/tortoise/backends/odbc/client.py @@ -123,7 +123,7 @@ def acquire_connection(self) -> ConnWrapperType: @translate_exceptions async def execute_many(self, query: str, values: list) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) async with connection.cursor() as cursor: try: await cursor.executemany(query, values) @@ -136,7 +136,7 @@ async def execute_many(self, query: str, values: list) -> None: @translate_exceptions async def execute_query(self, query: str, values: list | None = None) -> tuple[int, list[dict]]: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) async with connection.cursor() as cursor: if values: await cursor.execute(query, values) @@ -184,7 +184,7 @@ def acquire_connection(self) -> ConnWrapperType: @translate_exceptions async def execute_many(self, query: str, values: list) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) cursor = await connection.cursor() await cursor.executemany(query, values) diff --git a/tortoise/backends/oracle/client.py b/tortoise/backends/oracle/client.py index cd3a762d4..be6e61672 100644 --- a/tortoise/backends/oracle/client.py +++ b/tortoise/backends/oracle/client.py @@ -92,7 +92,7 @@ async def execute_script(self, query: str) -> None: @translate_exceptions async def execute_insert(self, query: str, values: list) -> int: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) await connection.execute(query, values) return 0 diff --git a/tortoise/backends/psycopg/client.py b/tortoise/backends/psycopg/client.py index 7acbf956f..3cd4e7bf4 100644 --- a/tortoise/backends/psycopg/client.py +++ b/tortoise/backends/psycopg/client.py @@ -133,7 +133,7 @@ async def execute_many(self, query: str, values: list) -> None: connection: psycopg.AsyncConnection async with self.acquire_connection() as connection: async with connection.cursor() as cursor: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) await cursor.executemany(query, values) @postgres_client.translate_exceptions @@ -147,7 +147,7 @@ async def execute_query( async with self.acquire_connection() as connection: cursor: psycopg.AsyncCursor | psycopg.AsyncServerCursor async with connection.cursor(row_factory=row_factory) as cursor: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) await cursor.execute(query, values) rowcount = int(cursor.rowcount or cursor.rownumber or 0) diff --git a/tortoise/backends/sqlite/client.py b/tortoise/backends/sqlite/client.py index dc8ac09b5..68971a4ed 100644 --- a/tortoise/backends/sqlite/client.py +++ b/tortoise/backends/sqlite/client.py @@ -127,13 +127,13 @@ def _in_transaction(self) -> TransactionContext: @translate_exceptions async def execute_insert(self, query: str, values: list) -> int: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) return (await connection.execute_insert(query, values))[0] @translate_exceptions async def execute_many(self, query: str, values: list[list]) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) # This code is only ever called in AUTOCOMMIT mode await connection.execute("BEGIN") try: @@ -150,7 +150,7 @@ async def execute_query( ) -> tuple[int, Sequence[dict]]: query = query.replace("\x00", "'||CHAR(0)||'") async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) start = connection.total_changes rows = await connection.execute_fetchall(query, values) return (connection.total_changes - start) or len(rows), rows @@ -159,7 +159,7 @@ async def execute_query( async def execute_query_dict(self, query: str, values: list | None = None) -> list[dict]: query = query.replace("\x00", "'||CHAR(0)||'") async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) return list(map(dict, await connection.execute_fetchall(query, values))) @translate_exceptions @@ -229,7 +229,7 @@ def _in_transaction(self) -> TransactionContext: @translate_exceptions async def execute_many(self, query: str, values: list[list]) -> None: async with self.acquire_connection() as connection: - self.log.debug("%s: %s", query, values) + self.log.debug("%s", query) # Already within transaction, so ideal for performance await connection.executemany(query, values) From e8ff19e952515e37b0a1bb823ae16f1b08bc54c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:57:00 +0000 Subject: [PATCH 3/8] Add comprehensive test for query parameter logging security fix Co-authored-by: bddap <2702854+bddap@users.noreply.github.com> --- tests/test_logging_security.py | 87 ++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_logging_security.py diff --git a/tests/test_logging_security.py b/tests/test_logging_security.py new file mode 100644 index 000000000..b04416a15 --- /dev/null +++ b/tests/test_logging_security.py @@ -0,0 +1,87 @@ +"""Test that sensitive data is not exposed in debug logging.""" +import logging +from io import StringIO + +from tests.testmodels import User +from tortoise.contrib.test import TestCase + + +class TestLoggingSecurity(TestCase): + """Test cases for ensuring sensitive data is not logged by Tortoise ORM.""" + + async def test_query_parameters_not_logged_in_tortoise_db_client(self): + """Test that query parameters are not logged by tortoise.db_client logger.""" + # Create a string IO to capture log output + log_capture = StringIO() + + # Get the tortoise db_client logger and add our handler + logger = logging.getLogger('tortoise.db_client') + original_level = logger.level + handler = logging.StreamHandler(log_capture) + handler.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(name)s:%(levelname)s:%(message)s') + handler.setFormatter(formatter) + + # Set up logging + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + + try: + # Create a user with potentially sensitive data + sensitive_email = "admin@secret-company.com" + sensitive_username = "admin_with_secret_key_123" + sensitive_bio = "bio with password: my_secret_password_123" + + user = await User.create( + username=sensitive_username, + mail=sensitive_email, + bio=sensitive_bio + ) + + # Get the captured log output + log_output = log_capture.getvalue() + + # Verify that the SQL query structure is still logged + self.assertIn("INSERT INTO", log_output) + self.assertIn("user", log_output.lower()) + + # Verify that sensitive data is NOT in the log output + self.assertNotIn(sensitive_email, log_output, + f"Sensitive email found in log output: {log_output}") + self.assertNotIn(sensitive_username, log_output, + f"Sensitive username found in log output: {log_output}") + self.assertNotIn(sensitive_bio, log_output, + f"Sensitive bio found in log output: {log_output}") + self.assertNotIn("my_secret_password_123", log_output, + f"Sensitive password found in log output: {log_output}") + + # Test UPDATE operation + log_capture.seek(0) # Reset the capture + log_capture.truncate(0) + + new_sensitive_email = "super_secret_admin@classified.gov" + user.mail = new_sensitive_email + await user.save() + + log_output = log_capture.getvalue() + self.assertNotIn(new_sensitive_email, log_output, + f"Sensitive email found in UPDATE log: {log_output}") + + # Test SELECT operation + log_capture.seek(0) # Reset the capture + log_capture.truncate(0) + + await User.filter(username=sensitive_username).first() + + log_output = log_capture.getvalue() + self.assertNotIn(sensitive_username, log_output, + f"Sensitive username found in SELECT log: {log_output}") + + # Clean up + await user.delete() + + finally: + # Restore original logging setup + logger.removeHandler(handler) + logger.setLevel(original_level) + handler.close() \ No newline at end of file From 79606f1d8d3280a28848f6278936cc34071596a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:03:20 +0000 Subject: [PATCH 4/8] Add changelog entry for security fix #1996 Co-authored-by: bddap <2702854+bddap@users.noreply.github.com> --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a0f865d09..a50160268 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,10 @@ Changelog 0.25.1 ------------------ +Fixed +^^^^^ +- Remove sensitive query parameters from debug logging to prevent exposure of passwords, tokens, and personal data (#1996) + Changed ^^^^^ - Force async task switch every 2000 rows when converting db objects to python objects to avoid blocking the event loop (#1939) From 9a7c34e526aa6fee6475f85641d5aab9208fd283 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:20:37 +0000 Subject: [PATCH 5/8] Fix changelog structure and format test file per review feedback Co-authored-by: bddap <2702854+bddap@users.noreply.github.com> --- CHANGELOG.rst | 6 ++- tests/test_logging_security.py | 79 ++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a50160268..211b6091e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,12 +10,14 @@ Changelog 0.25 ==== -0.25.1 ------------------- +0.25.2 +------ Fixed ^^^^^ - Remove sensitive query parameters from debug logging to prevent exposure of passwords, tokens, and personal data (#1996) +0.25.1 +------ Changed ^^^^^ - Force async task switch every 2000 rows when converting db objects to python objects to avoid blocking the event loop (#1939) diff --git a/tests/test_logging_security.py b/tests/test_logging_security.py index b04416a15..8bd196b83 100644 --- a/tests/test_logging_security.py +++ b/tests/test_logging_security.py @@ -1,4 +1,5 @@ """Test that sensitive data is not exposed in debug logging.""" + import logging from io import StringIO @@ -13,75 +14,87 @@ async def test_query_parameters_not_logged_in_tortoise_db_client(self): """Test that query parameters are not logged by tortoise.db_client logger.""" # Create a string IO to capture log output log_capture = StringIO() - + # Get the tortoise db_client logger and add our handler - logger = logging.getLogger('tortoise.db_client') + logger = logging.getLogger("tortoise.db_client") original_level = logger.level handler = logging.StreamHandler(log_capture) handler.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(name)s:%(levelname)s:%(message)s') + formatter = logging.Formatter("%(name)s:%(levelname)s:%(message)s") handler.setFormatter(formatter) - + # Set up logging logger.setLevel(logging.DEBUG) logger.addHandler(handler) - + try: # Create a user with potentially sensitive data sensitive_email = "admin@secret-company.com" sensitive_username = "admin_with_secret_key_123" sensitive_bio = "bio with password: my_secret_password_123" - + user = await User.create( - username=sensitive_username, - mail=sensitive_email, - bio=sensitive_bio + username=sensitive_username, mail=sensitive_email, bio=sensitive_bio ) - + # Get the captured log output log_output = log_capture.getvalue() - + # Verify that the SQL query structure is still logged self.assertIn("INSERT INTO", log_output) self.assertIn("user", log_output.lower()) - + # Verify that sensitive data is NOT in the log output - self.assertNotIn(sensitive_email, log_output, - f"Sensitive email found in log output: {log_output}") - self.assertNotIn(sensitive_username, log_output, - f"Sensitive username found in log output: {log_output}") - self.assertNotIn(sensitive_bio, log_output, - f"Sensitive bio found in log output: {log_output}") - self.assertNotIn("my_secret_password_123", log_output, - f"Sensitive password found in log output: {log_output}") - + self.assertNotIn( + sensitive_email, log_output, f"Sensitive email found in log output: {log_output}" + ) + self.assertNotIn( + sensitive_username, + log_output, + f"Sensitive username found in log output: {log_output}", + ) + self.assertNotIn( + sensitive_bio, log_output, f"Sensitive bio found in log output: {log_output}" + ) + self.assertNotIn( + "my_secret_password_123", + log_output, + f"Sensitive password found in log output: {log_output}", + ) + # Test UPDATE operation log_capture.seek(0) # Reset the capture log_capture.truncate(0) - + new_sensitive_email = "super_secret_admin@classified.gov" user.mail = new_sensitive_email await user.save() - + log_output = log_capture.getvalue() - self.assertNotIn(new_sensitive_email, log_output, - f"Sensitive email found in UPDATE log: {log_output}") - + self.assertNotIn( + new_sensitive_email, + log_output, + f"Sensitive email found in UPDATE log: {log_output}", + ) + # Test SELECT operation log_capture.seek(0) # Reset the capture log_capture.truncate(0) - + await User.filter(username=sensitive_username).first() - + log_output = log_capture.getvalue() - self.assertNotIn(sensitive_username, log_output, - f"Sensitive username found in SELECT log: {log_output}") - + self.assertNotIn( + sensitive_username, + log_output, + f"Sensitive username found in SELECT log: {log_output}", + ) + # Clean up await user.delete() - + finally: # Restore original logging setup logger.removeHandler(handler) logger.setLevel(original_level) - handler.close() \ No newline at end of file + handler.close() From 5b9280e6e03edb9b3c73d90202faa5b0f820f6d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:57:44 +0000 Subject: [PATCH 6/8] Use [Unreleased] section in changelog following Keep A Changelog format Co-authored-by: bddap <2702854+bddap@users.noreply.github.com> --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 211b6091e..55eef59f0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,8 +10,8 @@ Changelog 0.25 ==== -0.25.2 ------- +[Unreleased] +------------ Fixed ^^^^^ - Remove sensitive query parameters from debug logging to prevent exposure of passwords, tokens, and personal data (#1996) From ddbcc7270c9cdd246703e431c8331ec4bc1c6085 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:06:19 +0000 Subject: [PATCH 7/8] Fix changelog structure: Move [Unreleased] section to top level Co-authored-by: bddap <2702854+bddap@users.noreply.github.com> --- CHANGELOG.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 55eef59f0..c1c3e3b9a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,15 +7,15 @@ Changelog .. rst-class:: emphasize-children -0.25 -==== - [Unreleased] ------------- +============ Fixed -^^^^^ +----- - Remove sensitive query parameters from debug logging to prevent exposure of passwords, tokens, and personal data (#1996) +0.25 +==== + 0.25.1 ------ Changed From 814974b9b66664562fd9e20e0fd322d1f2c3cab3 Mon Sep 17 00:00:00 2001 From: Andrew Dirksen Date: Fri, 22 Aug 2025 18:49:18 +0000 Subject: [PATCH 8/8] use the repo's current changlog style --- CHANGELOG.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8d3fefd7..f03246bdf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,17 +7,14 @@ Changelog .. rst-class:: emphasize-children -[Unreleased] -============ -Fixed ------ -- Remove sensitive query parameters from debug logging to prevent exposure of passwords, tokens, and personal data (#1996) - 0.26 ==== 0.26.0 (unreleased) -------------------- +------------------- +Fixed +^^^^^ +- Remove sensitive query parameters from debug logging to prevent exposure of passwords, tokens, and personal data (#1996) Added ^^^^^ - Add `create()` method to reverse ForeignKey relations, enabling `parent.children.create()` syntax