From ae7084b80471c8b1742e374602a595c3b184cef8 Mon Sep 17 00:00:00 2001 From: devdudumuniz <82589615+devdudumuniz@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:12:53 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Fix=20SQL=20injection=20vulnerab?= =?UTF-8?q?ility=20in=20DuckDB=20`read=5Fparquet`=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced direct string interpolation of file paths with DuckDB's parameterized queries (`?`) in both the schema checking logic (`get_columns`) and the main query execution block within `pysus/api/client.py`. This securely handles lists of paths directly through DuckDB's parameter binding, eliminating the risk of SQL injection while preserving `sql` parameter modification functionality. --- pysus/api/client.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pysus/api/client.py b/pysus/api/client.py index 982fbde9..5fe4f606 100644 --- a/pysus/api/client.py +++ b/pysus/api/client.py @@ -820,14 +820,12 @@ def read_parquet( def get_columns(path: Path) -> set[tuple[str, str]]: """Return the schema of a Parquet file as (name, type) pairs.""" - result = duckdb.execute(f"SELECT * FROM '{path}' LIMIT 0") + result = duckdb.execute( + "SELECT * FROM read_parquet(?) LIMIT 0", [[str(path)]] + ) return {(col[0], str(col[1])) for col in result.description} - if len(paths) == 1: - query = f"SELECT * FROM '{paths[0]}'" - else: - paths_str = ", ".join(f"'{p}'" for p in paths) - query = f"SELECT * FROM read_parquet([{paths_str}])" + query = "SELECT * FROM read_parquet(?)" schemas = [get_columns(p) for p in paths] common_columns = set.intersection(*schemas) if schemas else set() @@ -847,22 +845,24 @@ def get_columns(path: Path) -> set[tuple[str, str]]: if not common_columns: return duckdb.execute("SELECT * WHERE 1=0") cols = ", ".join(f'"{c[0]}"' for c in sorted(common_columns)) - paths_str = ", ".join(f"'{p}'" for p in paths) - query = f"SELECT {cols} FROM read_parquet([{paths_str}])" + query = f"SELECT {cols} FROM read_parquet(?)" else: - paths_str = ", ".join(f"'{p}'" for p in paths) - query = ( - f"SELECT * FROM read_parquet([{paths_str}], union_by_name=True)" - ) + query = "SELECT * FROM read_parquet(?, union_by_name=True)" + + paths_list = [str(p) for p in paths] + params = [paths_list] if sql: if sql.upper().startswith("SELECT"): + num_replacements = sql.count("FROM t") + if num_replacements > 1: + params = params * num_replacements query = sql.replace("FROM t", f"FROM ({query}) AS t") else: query = f"SELECT {sql} FROM ({query}) AS t" - base = duckdb.execute(query) + base = duckdb.execute(query, params) if not add_dv: return base @@ -890,4 +890,4 @@ def get_columns(path: Path) -> set[tuple[str, str]]: for c in base.description ] query = f"SELECT {', '.join(selects)} FROM ({query}) AS _t" - return duckdb.execute(query) + return duckdb.execute(query, params)