This is an automated email from the ASF dual-hosted git repository.

FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git


The following commit(s) were added to refs/heads/master by this push:
     new 57a7291  fix: audit and harden dynamic SQL sinks (#128)
57a7291 is described below

commit 57a7291ad0c773ad5eb35d2d7d156df49302266f
Author: Yijia Su <[email protected]>
AuthorDate: Thu Jul 30 00:49:43 2026 +0800

    fix: audit and harden dynamic SQL sinks (#128)
---
 doris_mcp_server/tools/resources_manager.py        |  38 ++-
 doris_mcp_server/utils/analysis_tools.py           |  73 ++++--
 doris_mcp_server/utils/data_exploration_tools.py   | 152 ++++++++----
 doris_mcp_server/utils/data_governance_tools.py    |  73 ++++--
 doris_mcp_server/utils/data_quality_tools.py       | 219 +++++++++++-----
 .../utils/dependency_analysis_tools.py             |   6 +-
 .../utils/performance_analytics_tools.py           |  67 +++--
 doris_mcp_server/utils/schema_extractor.py         | 166 ++++++++-----
 doris_mcp_server/utils/security_analytics_tools.py |  43 +++-
 doris_mcp_server/utils/sql_security_utils.py       |  85 ++++++-
 test/integration/test_real_doris_transports.py     |  60 +++++
 test/protocol/stdio_capability_server.py           |  35 +++
 test/protocol/test_mcp_v2_protocol.py              |  76 ++++++
 test/security/test_sql_sink_audit.py               | 276 +++++++++++++++++++++
 14 files changed, 1110 insertions(+), 259 deletions(-)

diff --git a/doris_mcp_server/tools/resources_manager.py 
b/doris_mcp_server/tools/resources_manager.py
index b481c36..482acb1 100644
--- a/doris_mcp_server/tools/resources_manager.py
+++ b/doris_mcp_server/tools/resources_manager.py
@@ -30,7 +30,7 @@ from mcp.types import Resource
 from ..utils.db import DorisConnectionManager
 from ..utils.logger import get_logger
 from ..utils.redaction import redact_uri
-from ..utils.sql_security_utils import get_auth_context
+from ..utils.sql_security_utils import get_auth_context, quote_identifier
 
 
 logger = get_logger(__name__)
@@ -141,9 +141,10 @@ class DorisResourcesManager:
         return getattr(get_auth_context(), "auth_method", "") == "doris_oauth"
 
     def _schema_filter(self, column_name: str, db_name: str | None) -> 
tuple[str, tuple]:
+        safe_column = quote_identifier(column_name, "information schema 
column")
         if db_name:
-            return f"{column_name} = %s", (db_name,)
-        return f"{column_name} = DATABASE()", ()
+            return f"{safe_column} = %s", (db_name,)
+        return f"{safe_column} = DATABASE()", ()
 
     def _first_row_value(self, row: Any) -> Any:
         if isinstance(row, dict):
@@ -383,6 +384,8 @@ class DorisResourcesManager:
     ) -> list[TableMetadata]:
         """Get table metadata for a specific database or the current 
database."""
         schema_filter, schema_params = self._schema_filter("table_schema", 
db_name)
+        # SQL sink audit: schema column -> quote_identifier and database value
+        # -> bound param; fixed template -> DorisConnection.execute.
         tables_query = """
         SELECT
             table_name,
@@ -393,7 +396,7 @@ class DorisResourcesManager:
         WHERE {schema_filter}
         AND table_type = 'BASE TABLE'
         ORDER BY table_name
-        """.format(schema_filter=schema_filter)
+        """.format(schema_filter=schema_filter)  # nosec B608
 
         auth_context = get_auth_context()
         result = await connection.execute(
@@ -423,6 +426,8 @@ class DorisResourcesManager:
     ) -> list[dict]:
         """Get column information for table"""
         schema_filter, schema_params = self._schema_filter("table_schema", 
db_name)
+        # SQL sink audit: schema column -> quote_identifier; database/table
+        # values -> bound params; fixed template -> DorisConnection.execute.
         columns_query = """
         SELECT
             column_name,
@@ -435,7 +440,7 @@ class DorisResourcesManager:
         WHERE {schema_filter}
         AND table_name = %s
         ORDER BY ordinal_position
-        """.format(schema_filter=schema_filter)
+        """.format(schema_filter=schema_filter)  # nosec B608
 
         auth_context = get_auth_context()
         result = await connection.execute(
@@ -463,6 +468,8 @@ class DorisResourcesManager:
     ) -> list[ViewMetadata]:
         """Get view metadata for a specific database or the current 
database."""
         schema_filter, schema_params = self._schema_filter("table_schema", 
db_name)
+        # SQL sink audit: schema column -> quote_identifier and database value
+        # -> bound param; fixed template -> DorisConnection.execute.
         views_query = """
         SELECT
             table_name,
@@ -471,7 +478,7 @@ class DorisResourcesManager:
         FROM information_schema.views
         WHERE {schema_filter}
         ORDER BY table_name
-        """.format(schema_filter=schema_filter)
+        """.format(schema_filter=schema_filter)  # nosec B608
 
         auth_context = get_auth_context()
         result = await connection.execute(
@@ -496,6 +503,8 @@ class DorisResourcesManager:
         """Get detailed structure information of table"""
         schema_filter, schema_params = self._schema_filter("table_schema", 
db_name)
         # Get basic table information
+        # SQL sink audit: schema column -> quote_identifier; database/table
+        # values -> bound params; fixed template -> DorisConnection.execute.
         table_info_query = """
         SELECT
             table_name,
@@ -506,7 +515,7 @@ class DorisResourcesManager:
         FROM information_schema.tables
         WHERE {schema_filter}
         AND table_name = %s
-        """.format(schema_filter=schema_filter)
+        """.format(schema_filter=schema_filter)  # nosec B608
 
         async with self._connection_context("system") as connection:
             auth_context = get_auth_context()
@@ -545,6 +554,8 @@ class DorisResourcesManager:
     ) -> list[dict]:
         """Get index information for table"""
         schema_filter, schema_params = self._schema_filter("table_schema", 
db_name)
+        # SQL sink audit: schema column -> quote_identifier; database/table
+        # values -> bound params; fixed template -> DorisConnection.execute.
         indexes_query = """
         SELECT
             index_name,
@@ -555,7 +566,7 @@ class DorisResourcesManager:
         WHERE {schema_filter}
         AND table_name = %s
         ORDER BY index_name, seq_in_index
-        """.format(schema_filter=schema_filter)
+        """.format(schema_filter=schema_filter)  # nosec B608
 
         auth_context = get_auth_context()
         result = await connection.execute(
@@ -568,6 +579,8 @@ class DorisResourcesManager:
     async def _get_view_definition(self, view_name: str, db_name: str | None = 
None) -> str:
         """Get definition information of view"""
         schema_filter, schema_params = self._schema_filter("table_schema", 
db_name)
+        # SQL sink audit: schema column -> quote_identifier; database/view
+        # values -> bound params; fixed template -> DorisConnection.execute.
         view_query = """
         SELECT
             table_name,
@@ -576,7 +589,7 @@ class DorisResourcesManager:
         FROM information_schema.views
         WHERE {schema_filter}
         AND table_name = %s
-        """.format(schema_filter=schema_filter)
+        """.format(schema_filter=schema_filter)  # nosec B608
 
         async with self._connection_context("system") as connection:
             auth_context = get_auth_context()
@@ -604,6 +617,8 @@ class DorisResourcesManager:
         """Get database statistics"""
         schema_filter, schema_params = self._schema_filter("table_schema", 
db_name)
         # Get table statistics
+        # SQL sink audit: schema column -> quote_identifier and database value
+        # -> bound param; fixed template -> DorisConnection.execute.
         table_stats_query = """
         SELECT
             COUNT(*) as table_count,
@@ -611,7 +626,7 @@ class DorisResourcesManager:
         FROM information_schema.tables
         WHERE {schema_filter}
         AND table_type = 'BASE TABLE'
-        """.format(schema_filter=schema_filter)
+        """.format(schema_filter=schema_filter)  # nosec B608
 
         async with self._connection_context("system") as connection:
             auth_context = get_auth_context()
@@ -623,11 +638,12 @@ class DorisResourcesManager:
             table_stats = table_result.data[0] if table_result.data else {}
 
             # Get view statistics
+            # SQL sink audit: same quoted schema column and bound database 
value.
             view_stats_query = """
             SELECT COUNT(*) as view_count
             FROM information_schema.views
             WHERE {schema_filter}
-            """.format(schema_filter=schema_filter)
+            """.format(schema_filter=schema_filter)  # nosec B608
 
             view_result = await connection.execute(
                 view_stats_query,
diff --git a/doris_mcp_server/utils/analysis_tools.py 
b/doris_mcp_server/utils/analysis_tools.py
index ecf33a7..070e861 100644
--- a/doris_mcp_server/utils/analysis_tools.py
+++ b/doris_mcp_server/utils/analysis_tools.py
@@ -34,7 +34,8 @@ from .sql_security_utils import (
     validate_identifier,
     quote_identifier,
     build_table_reference,
-    get_auth_context
+    get_auth_context,
+    validate_integer,
 )
 
 logger = get_logger(__name__)
@@ -107,9 +108,19 @@ class TableAnalyzer:
         }
         
         # Get sample data using quoted identifier
-        if include_sample and sample_size > 0:
+        safe_sample_size = validate_integer(
+            sample_size,
+            "sample size",
+            minimum=0,
+            maximum=10000,
+        )
+        if include_sample and safe_sample_size > 0:
             quoted_table = quote_identifier(table_name, "table name")
-            sample_sql = f"SELECT * FROM {quoted_table} LIMIT {sample_size}"
+            # SQL sink audit: caller integer -> bounded validation + quoted
+            # identifier -> DorisConnection.execute.
+            sample_sql = (
+                f"SELECT * FROM {quoted_table} LIMIT {safe_sample_size}"  # 
nosec B608
+            )
             sample_result = await connection.execute(sample_sql, 
auth_context=auth_context)
             summary["sample_data"] = sample_result.data
         
@@ -123,20 +134,28 @@ class TableAnalyzer:
     ) -> Dict[str, Any]:
         """Analyze column statistics"""
         try:
+            safe_table = quote_identifier(table_name, "table name")
+            safe_column = quote_identifier(column_name, "column name")
             connection = await self.connection_manager.get_connection("query")
             
             # Basic statistics
+            # SQL sink audit: MCP identifiers -> quote_identifier; label value
+            # -> bound parameter -> DorisConnection.execute.
             basic_stats_sql = f"""
             SELECT 
-                '{column_name}' as column_name,
+                %s as column_name,
                 COUNT(*) as total_count,
-                COUNT({column_name}) as non_null_count,
-                COUNT(DISTINCT {column_name}) as distinct_count
-            FROM {table_name}
-            """
+                COUNT({safe_column}) as non_null_count,
+                COUNT(DISTINCT {safe_column}) as distinct_count
+            FROM {safe_table}
+            """  # nosec B608
             
             auth_context = get_auth_context()
-            basic_result = await connection.execute(basic_stats_sql, 
auth_context=auth_context)
+            basic_result = await connection.execute(
+                basic_stats_sql,
+                params=(column_name,),
+                auth_context=auth_context,
+            )
             if not basic_result.data:
                 return {
                     "success": False,
@@ -149,16 +168,18 @@ class TableAnalyzer:
         
             if analysis_type in ["distribution", "detailed"]:
                 # Data distribution analysis
+                # SQL sink audit: MCP identifiers -> quote_identifier ->
+                # DorisConnection.execute.
                 distribution_sql = f"""
                 SELECT 
-                    {column_name} as value,
+                    {safe_column} as value,
                     COUNT(*) as frequency
-                FROM {table_name}
-                WHERE {column_name} IS NOT NULL
-                GROUP BY {column_name}
+                FROM {safe_table}
+                WHERE {safe_column} IS NOT NULL
+                GROUP BY {safe_column}
                 ORDER BY frequency DESC
                 LIMIT 20
-                """
+                """  # nosec B608
                 
                 distribution_result = await 
connection.execute(distribution_sql, auth_context=auth_context)
                 analysis["value_distribution"] = distribution_result.data
@@ -166,14 +187,16 @@ class TableAnalyzer:
             if analysis_type == "detailed":
                 # Detailed statistics (for numeric types)
                 try:
+                    # SQL sink audit: MCP identifiers -> quote_identifier ->
+                    # DorisConnection.execute.
                     numeric_stats_sql = f"""
                     SELECT 
-                        MIN({column_name}) as min_value,
-                        MAX({column_name}) as max_value,
-                        AVG({column_name}) as avg_value
-                    FROM {table_name}
-                    WHERE {column_name} IS NOT NULL
-                    """
+                        MIN({safe_column}) as min_value,
+                        MAX({safe_column}) as max_value,
+                        AVG({safe_column}) as avg_value
+                    FROM {safe_table}
+                    WHERE {safe_column} IS NOT NULL
+                    """  # nosec B608
                     
                     numeric_result = await 
connection.execute(numeric_stats_sql, auth_context=auth_context)
                     if numeric_result.data:
@@ -202,18 +225,22 @@ class TableAnalyzer:
         connection = await self.connection_manager.get_connection("system")
         
         # Get table basic information
-        table_info_sql = f"""
+        table_info_sql = """
         SELECT 
             table_name,
             table_comment,
             table_rows
         FROM information_schema.tables 
         WHERE table_schema = DATABASE()
-        AND table_name = '{table_name}'
+        AND table_name = %s
         """
         
         auth_context = get_auth_context()
-        table_result = await connection.execute(table_info_sql, 
auth_context=auth_context)
+        table_result = await connection.execute(
+            table_info_sql,
+            params=(table_name,),
+            auth_context=auth_context,
+        )
         if not table_result.data:
             raise ValueError(f"Table {table_name} does not exist")
         
diff --git a/doris_mcp_server/utils/data_exploration_tools.py 
b/doris_mcp_server/utils/data_exploration_tools.py
index b3e2a76..ce25aeb 100644
--- a/doris_mcp_server/utils/data_exploration_tools.py
+++ b/doris_mcp_server/utils/data_exploration_tools.py
@@ -29,6 +29,7 @@ from .logger import get_logger
 from .sql_security_utils import (
     SQLSecurityError,
     validate_identifier,
+    validate_integer,
     quote_identifier,
     build_table_reference,
     get_auth_context
@@ -65,7 +66,9 @@ class DataExplorationTools:
             # table_name should already be validated by _build_full_table_name
             auth_context = get_auth_context()
             
-            count_sql = f"SELECT COUNT(*) as row_count FROM {table_name}"
+            # SQL sink audit: table_name is produced by build_table_reference
+            # before this private helper reaches DorisConnection.execute.
+            count_sql = f"SELECT COUNT(*) as row_count FROM {table_name}"  # 
nosec B608
             result = await connection.execute(count_sql, 
auth_context=auth_context)
             
             if result.data:
@@ -102,6 +105,8 @@ class DataExplorationTools:
             else:
                 where_conditions.append("table_schema = DATABASE()")
             
+            # SQL sink audit: WHERE fragments are fixed locally and all values
+            # are bound through params before DorisConnection.execute.
             columns_sql = f"""
             SELECT 
                 column_name,
@@ -112,7 +117,7 @@ class DataExplorationTools:
             FROM information_schema.columns 
             WHERE {' AND '.join(where_conditions)}
             ORDER BY ordinal_position
-            """
+            """  # nosec B608
             
             result = await connection.execute(columns_sql, 
params=tuple(params), auth_context=auth_context)
             return result.data if result.data else []
@@ -126,7 +131,13 @@ class DataExplorationTools:
     
     async def _determine_sampling_strategy(self, connection, table_name: str, 
total_rows: int, sample_size: int) -> Dict[str, Any]:
         """Determine optimal sampling strategy based on table size"""
-        if total_rows <= sample_size:
+        safe_sample_size = validate_integer(
+            sample_size,
+            "sample size",
+            minimum=0,
+            maximum=10_000_000,
+        )
+        if total_rows <= safe_sample_size:
             # Use all data if table is small enough
             return {
                 "total_rows": total_rows,
@@ -138,14 +149,19 @@ class DataExplorationTools:
             }
         else:
             # Use random sampling for large tables
-            sampling_ratio = sample_size / total_rows
+            sampling_ratio = safe_sample_size / total_rows
             return {
                 "total_rows": total_rows,
-                "sample_size": sample_size,
+                "sample_size": safe_sample_size,
                 "sampling_method": "random_sample",
                 "sampling_ratio": round(sampling_ratio, 4),
                 "use_sampling": True,
-                "sample_table_expression": f"(SELECT * FROM {table_name} ORDER 
BY RAND() LIMIT {sample_size}) as sample_table"
+                # SQL sink audit: table -> build_table_reference; sample size 
->
+                # bounded integer; expression remains internal until execute.
+                "sample_table_expression": (
+                    f"(SELECT * FROM {table_name} ORDER BY RAND() "
+                    f"LIMIT {safe_sample_size}) as sample_table"  # nosec B608
+                ),
             }
     
     def _select_analysis_columns(self, columns_info: List[Dict], include_all: 
bool) -> List[Dict]:
@@ -194,18 +210,22 @@ class DataExplorationTools:
         for column in numeric_columns:
             col_name = column["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
                 # Basic statistics
                 table_expr = sampling_info.get("sample_table_expression", 
table_name)
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 stats_sql = f"""
                 SELECT 
-                    COUNT({col_name}) as count,
-                    MIN({col_name}) as min_value,
-                    MAX({col_name}) as max_value,
-                    AVG({col_name}) as mean_value,
-                    STDDEV({col_name}) as std_dev
+                    COUNT({safe_column}) as count,
+                    MIN({safe_column}) as min_value,
+                    MAX({safe_column}) as max_value,
+                    AVG({safe_column}) as mean_value,
+                    STDDEV({safe_column}) as std_dev
                 FROM {table_expr}
-                WHERE {col_name} IS NOT NULL
-                """
+                WHERE {safe_column} IS NOT NULL
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 stats_result = await connection.execute(stats_sql, 
auth_context=auth_context)
@@ -247,18 +267,22 @@ class DataExplorationTools:
     async def _calculate_percentiles(self, connection, table_name: str, 
col_name: str, sampling_info: Dict) -> Dict[str, float]:
         """Calculate percentiles for numeric column"""
         try:
+            safe_column = quote_identifier(col_name, "column name")
             table_expr = sampling_info.get("sample_table_expression", 
table_name)
+            # SQL sink audit: metadata column -> quote_identifier; table_expr
+            # originates from a safe table/sampler before
+            # DorisConnection.execute.
             percentile_sql = f"""
             SELECT 
-                PERCENTILE({col_name}, 0.25) as p25,
-                PERCENTILE({col_name}, 0.50) as p50,
-                PERCENTILE({col_name}, 0.75) as p75,
-                PERCENTILE({col_name}, 0.90) as p90,
-                PERCENTILE({col_name}, 0.95) as p95,
-                PERCENTILE({col_name}, 0.99) as p99
+                PERCENTILE({safe_column}, 0.25) as p25,
+                PERCENTILE({safe_column}, 0.50) as p50,
+                PERCENTILE({safe_column}, 0.75) as p75,
+                PERCENTILE({safe_column}, 0.90) as p90,
+                PERCENTILE({safe_column}, 0.95) as p95,
+                PERCENTILE({safe_column}, 0.99) as p99
             FROM {table_expr}
-            WHERE {col_name} IS NOT NULL
-            """
+            WHERE {safe_column} IS NOT NULL
+            """  # nosec B608
             
             auth_context = get_auth_context()
             result = await connection.execute(percentile_sql, 
auth_context=auth_context)
@@ -291,17 +315,25 @@ class DataExplorationTools:
             lower_bound = q1 - 1.5 * iqr
             upper_bound = q3 + 1.5 * iqr
             
+            safe_column = quote_identifier(col_name, "column name")
             table_expr = sampling_info.get("sample_table_expression", 
table_name)
+            # SQL sink audit: metadata column -> quote_identifier; numeric
+            # thresholds -> bound params; table_expr -> safe table/sampler.
             outlier_sql = f"""
             SELECT 
                 COUNT(*) as total_count,
-                SUM(CASE WHEN {col_name} < {lower_bound} OR {col_name} > 
{upper_bound} THEN 1 ELSE 0 END) as outlier_count
+                SUM(CASE WHEN {safe_column} < %s OR {safe_column} > %s
+                    THEN 1 ELSE 0 END) as outlier_count
             FROM {table_expr}
-            WHERE {col_name} IS NOT NULL
-            """
+            WHERE {safe_column} IS NOT NULL
+            """  # nosec B608
             
             auth_context = get_auth_context()
-            result = await connection.execute(outlier_sql, 
auth_context=auth_context)
+            result = await connection.execute(
+                outlier_sql,
+                params=(lower_bound, upper_bound),
+                auth_context=auth_context,
+            )
             
             if result.data:
                 data = result.data[0]
@@ -382,15 +414,17 @@ class DataExplorationTools:
         for column in categorical_columns:
             col_name = column["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
                 # Basic cardinality and distribution
+                # SQL sink audit: metadata column -> quote_identifier; table ->
+                # build_table_reference -> DorisConnection.execute.
                 cardinality_sql = f"""
                 SELECT 
-                    COUNT(DISTINCT {col_name}) as cardinality,
-                    COUNT({col_name}) as non_null_count
+                    COUNT(DISTINCT {safe_column}) as cardinality,
+                    COUNT({safe_column}) as non_null_count
                 FROM {table_name}
-                WHERE {col_name} IS NOT NULL
-                {sampling_info.get('sample_query_suffix', '')}
-                """
+                WHERE {safe_column} IS NOT NULL
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 cardinality_result = await connection.execute(cardinality_sql, 
auth_context=auth_context)
@@ -430,17 +464,20 @@ class DataExplorationTools:
         try:
             # Use sample table expression if sampling is enabled
             table_expr = sampling_info.get("sample_table_expression", 
table_name)
+            safe_column = quote_identifier(col_name, "column name")
             
+            # SQL sink audit: metadata column -> quote_identifier; table_expr
+            # originates from a safe table/sampler.
             distribution_sql = f"""
             SELECT 
-                {col_name} as value,
+                {safe_column} as value,
                 COUNT(*) as count
             FROM {table_expr}
-            WHERE {col_name} IS NOT NULL
-            GROUP BY {col_name}
+            WHERE {safe_column} IS NOT NULL
+            GROUP BY {safe_column}
             ORDER BY COUNT(*) DESC
             LIMIT 20
-            """
+            """  # nosec B608
             
             auth_context = get_auth_context()
             result = await connection.execute(distribution_sql, 
auth_context=auth_context)
@@ -482,16 +519,20 @@ class DataExplorationTools:
         for column in temporal_columns:
             col_name = column["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
                 # Date range analysis
                 table_expr = sampling_info.get("sample_table_expression", 
table_name)
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 range_sql = f"""
                 SELECT 
-                    MIN({col_name}) as earliest,
-                    MAX({col_name}) as latest,
-                    COUNT({col_name}) as non_null_count
+                    MIN({safe_column}) as earliest,
+                    MAX({safe_column}) as latest,
+                    COUNT({safe_column}) as non_null_count
                 FROM {table_expr}
-                WHERE {col_name} IS NOT NULL
-                """
+                WHERE {safe_column} IS NOT NULL
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 range_result = await connection.execute(range_sql, 
auth_context=auth_context)
@@ -564,16 +605,20 @@ class DataExplorationTools:
         """Analyze temporal patterns like seasonality and trends"""
         try:
             table_expr = sampling_info.get("sample_table_expression", 
table_name)
+            safe_column = quote_identifier(col_name, "column name")
             # Weekly pattern analysis
+            # SQL sink audit: metadata column -> quote_identifier; table_expr
+            # originates from a safe table/sampler before
+            # DorisConnection.execute.
             weekly_pattern_sql = f"""
             SELECT 
-                DAYOFWEEK({col_name}) as day_of_week,
+                DAYOFWEEK({safe_column}) as day_of_week,
                 COUNT(*) as count
             FROM {table_expr}
-            WHERE {col_name} IS NOT NULL
-            GROUP BY DAYOFWEEK({col_name})
+            WHERE {safe_column} IS NOT NULL
+            GROUP BY DAYOFWEEK({safe_column})
             ORDER BY day_of_week
-            """
+            """  # nosec B608
             
             auth_context = get_auth_context()
             weekly_result = await connection.execute(weekly_pattern_sql, 
auth_context=auth_context)
@@ -586,17 +631,18 @@ class DataExplorationTools:
                     weekly_pattern.append(round(percentage, 3))
             
             # Monthly trend analysis (simplified)
+            # SQL sink audit: same quoted metadata column and safe table_expr.
             monthly_trend_sql = f"""
             SELECT 
-                YEAR({col_name}) as year,
-                MONTH({col_name}) as month,
+                YEAR({safe_column}) as year,
+                MONTH({safe_column}) as month,
                 COUNT(*) as count
             FROM {table_expr}
-            WHERE {col_name} IS NOT NULL
-            GROUP BY YEAR({col_name}), MONTH({col_name})
+            WHERE {safe_column} IS NOT NULL
+            GROUP BY YEAR({safe_column}), MONTH({safe_column})
             ORDER BY year, month
             LIMIT 12
-            """
+            """  # nosec B608
             
             monthly_result = await connection.execute(monthly_trend_sql, 
auth_context=auth_context)
             monthly_trend = "stable"  # Simplified trend analysis
@@ -675,13 +721,17 @@ class DataExplorationTools:
         for column in columns:
             col_name = column["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
                 table_expr = sampling_info.get("sample_table_expression", 
table_name)
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 null_sql = f"""
                 SELECT 
                     COUNT(*) as total_count,
-                    COUNT({col_name}) as non_null_count
+                    COUNT({safe_column}) as non_null_count
                 FROM {table_expr}
-                """
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 result = await connection.execute(null_sql, 
auth_context=auth_context)
@@ -768,4 +818,4 @@ class DataExplorationTools:
         
         summary["notable_patterns"] = patterns
         
-        return summary 
\ No newline at end of file
+        return summary
diff --git a/doris_mcp_server/utils/data_governance_tools.py 
b/doris_mcp_server/utils/data_governance_tools.py
index 8887769..3609a73 100644
--- a/doris_mcp_server/utils/data_governance_tools.py
+++ b/doris_mcp_server/utils/data_governance_tools.py
@@ -28,6 +28,7 @@ from .db import DorisConnectionManager
 from .logger import get_logger
 from .sql_security_utils import (
     SQLSecurityError,
+    build_rule_predicate,
     validate_identifier,
     quote_identifier,
     build_table_reference,
@@ -253,7 +254,9 @@ class DataGovernanceTools:
             auth_context = get_auth_context()
             
             # Try to get table row count
-            count_sql = f"SELECT COUNT(*) as row_count FROM {table_name}"
+            # SQL sink audit: table_name is produced by build_table_reference
+            # before this private helper reaches DorisConnection.execute.
+            count_sql = f"SELECT COUNT(*) as row_count FROM {table_name}"  # 
nosec B608
             result = await connection.execute(count_sql, 
auth_context=auth_context)
             
             if result.data:
@@ -290,6 +293,8 @@ class DataGovernanceTools:
             else:
                 where_conditions.append("table_schema = DATABASE()")
             
+            # SQL sink audit: where fragments are selected locally and all 
values
+            # are bound through params before DorisConnection.execute.
             columns_sql = f"""
             SELECT 
                 column_name,
@@ -300,7 +305,7 @@ class DataGovernanceTools:
             FROM information_schema.columns 
             WHERE {' AND '.join(where_conditions)}
             ORDER BY ordinal_position
-            """
+            """  # nosec B608
             
             result = await connection.execute(columns_sql, 
params=tuple(params), auth_context=auth_context)
             return result.data if result.data else []
@@ -336,13 +341,15 @@ class DataGovernanceTools:
                 quoted_column = quote_identifier(column_name, "column name")
                 
                 # Calculate null value statistics
+                # SQL sink audit: metadata column -> quote_identifier; table ->
+                # build_table_reference -> DorisConnection.execute.
                 null_sql = f"""
                 SELECT 
                     COUNT(*) as total_count,
                     COUNT({quoted_column}) as non_null_count,
                     COUNT(*) - COUNT({quoted_column}) as null_count
                 FROM {table_name}
-                """
+                """  # nosec B608
                 
                 result = await connection.execute(null_sql, 
auth_context=auth_context)
                 if result.data:
@@ -383,22 +390,24 @@ class DataGovernanceTools:
         
         for rule in business_rules:
             rule_name = rule.get("rule_name", "unknown")
-            sql_condition = rule.get("sql_condition", "")
-            
-            if not sql_condition:
-                continue
-                
             try:
+                predicate, predicate_params = build_rule_predicate(rule)
                 # Check number of records meeting conditions
+                # SQL sink audit: structured rule -> allowlisted operator,
+                # quoted identifier and bound values; table -> safe reference.
                 compliance_sql = f"""
                 SELECT 
                     COUNT(*) as total_count,
-                    SUM(CASE WHEN {sql_condition} THEN 1 ELSE 0 END) as 
pass_count
+                    SUM(CASE WHEN {predicate} THEN 1 ELSE 0 END) as pass_count
                 FROM {table_name}
-                """
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
-                result = await connection.execute(compliance_sql, 
auth_context=auth_context)
+                result = await connection.execute(
+                    compliance_sql,
+                    params=predicate_params or None,
+                    auth_context=auth_context,
+                )
                 if result.data:
                     stats = result.data[0]
                     pass_count = stats["pass_count"] or 0
@@ -406,7 +415,10 @@ class DataGovernanceTools:
                     pass_rate = pass_count / total_rows if total_rows > 0 else 0
                     
                     compliance_results[rule_name] = {
-                        "rule_condition": sql_condition,
+                        "rule_condition": {
+                            "column": rule.get("column"),
+                            "operator": rule.get("operator", "="),
+                        },
                         "total_records": total_rows,
                         "pass_count": pass_count,
                         "fail_count": fail_count,
@@ -414,6 +426,12 @@ class DataGovernanceTools:
                         "compliance_score": round(pass_rate, 4)
                     }
                     
+            except SQLSecurityError as e:
+                logger.warning(f"Rejected unsafe business rule {rule_name}: 
{str(e)}")
+                compliance_results[rule_name] = {
+                    "error": str(e),
+                    "compliance_score": 0.0,
+                }
             except Exception as e:
                 logger.warning(f"Failed to check business rule {rule_name}: 
{str(e)}")
                 compliance_results[rule_name] = {
@@ -432,16 +450,19 @@ class DataGovernanceTools:
             primary_key_columns = [col["column_name"] for col in columns_info 
if "primary" in col.get("column_comment", "").lower()]
             
             for pk_col in primary_key_columns:
+                safe_pk_col = quote_identifier(pk_col, "column name")
+                # SQL sink audit: metadata column -> quote_identifier; table ->
+                # build_table_reference -> DorisConnection.execute.
                 duplicate_sql = f"""
                 SELECT COUNT(*) as duplicate_count
                 FROM (
-                    SELECT {pk_col}, COUNT(*) as cnt
+                    SELECT {safe_pk_col}, COUNT(*) as cnt
                     FROM {table_name}
-                    WHERE {pk_col} IS NOT NULL
-                    GROUP BY {pk_col}
+                    WHERE {safe_pk_col} IS NOT NULL
+                    GROUP BY {safe_pk_col}
                     HAVING COUNT(*) > 1
                 ) t
-                """
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 result = await connection.execute(duplicate_sql, 
auth_context=auth_context)
@@ -531,7 +552,11 @@ class DataGovernanceTools:
             
             safe_column = quote_identifier(column_name, "column name")
             # table_name is already safe (from _build_full_table_name)
-            verify_sql = f"SELECT {safe_column} FROM {table_name} LIMIT 1"
+            # SQL sink audit: column -> quote_identifier; table ->
+            # build_table_reference -> DorisConnection.execute.
+            verify_sql = (
+                f"SELECT {safe_column} FROM {table_name} LIMIT 1"  # nosec B608
+            )
             auth_context = get_auth_context()
             await connection.execute(verify_sql, auth_context=auth_context)
             return True
@@ -753,13 +778,15 @@ class DataGovernanceTools:
             else:
                 where_clause = "table_schema = DATABASE()"
             
+            # SQL sink audit: where_clause has two fixed local variants and the
+            # optional database value is bound before DorisConnection.execute.
             tables_sql = f"""
             SELECT table_name 
             FROM information_schema.tables 
             WHERE {where_clause}
                 AND table_type = 'BASE TABLE'
             ORDER BY table_name
-            """
+            """  # nosec B608
             
             result = await connection.execute(tables_sql, params=tuple(params) 
if params else None, auth_context=auth_context)
             return [row["table_name"] for row in result.data] if result.data 
else []
@@ -872,10 +899,16 @@ class DataGovernanceTools:
             timestamp_columns = await self._find_timestamp_columns(connection, 
table_name)
             
             if timestamp_columns:
+                safe_timestamp_column = quote_identifier(
+                    timestamp_columns[0],
+                    "column name",
+                )
+                # SQL sink audit: metadata column -> quote_identifier; table ->
+                # build_table_reference -> DorisConnection.execute.
                 max_time_sql = f"""
-                SELECT MAX({timestamp_columns[0]}) as last_update
+                SELECT MAX({safe_timestamp_column}) as last_update
                 FROM {table_name}
-                """
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 result = await connection.execute(max_time_sql, 
auth_context=auth_context)
diff --git a/doris_mcp_server/utils/data_quality_tools.py 
b/doris_mcp_server/utils/data_quality_tools.py
index bdcf4af..275fef0 100644
--- a/doris_mcp_server/utils/data_quality_tools.py
+++ b/doris_mcp_server/utils/data_quality_tools.py
@@ -34,6 +34,8 @@ from .config import DorisConfig
 from .sql_security_utils import (
     SQLSecurityError,
     validate_identifier,
+    validate_integer,
+    quote_identifier,
     build_table_reference,
     get_auth_context
 )
@@ -150,6 +152,12 @@ class DataQualityTools:
         """
         try:
             start_time = time.time()
+            sample_size = validate_integer(
+                sample_size,
+                "sample size",
+                minimum=0,
+                maximum=10_000_000,
+            )
             logger.info(f"🔍 Analyzing columns for table: {table_name}")
             logger.info(f"📊 Columns to analyze: {columns}")
             logger.info(f"🎯 Analysis types: {analysis_types}")
@@ -317,7 +325,9 @@ class DataQualityTools:
             auth_context = get_auth_context()
             
             # Try to get row count
-            count_sql = f"SELECT COUNT(*) as row_count FROM {table_name}"
+            # SQL sink audit: table_name is produced by build_table_reference
+            # before this private helper reaches DorisConnection.execute.
+            count_sql = f"SELECT COUNT(*) as row_count FROM {table_name}"  # 
nosec B608
             result = await connection.execute(count_sql, 
auth_context=auth_context)
             if result.data:
                 return {"row_count": result.data[0]["row_count"]}
@@ -388,6 +398,8 @@ class DataQualityTools:
             params.append(table_name)
             where_conditions.append("PARTITION_NAME IS NOT NULL")
             
+            # SQL sink audit: WHERE fragments are fixed locally and all values
+            # are bound through params before DorisConnection.execute.
             partition_sql = f"""
             SELECT 
                 PARTITION_NAME,
@@ -397,7 +409,7 @@ class DataQualityTools:
                 INDEX_LENGTH
             FROM information_schema.PARTITIONS 
             WHERE {' AND '.join(where_conditions)}
-            """
+            """  # nosec B608
             
             result = await connection.execute(partition_sql, 
params=tuple(params), auth_context=auth_context)
             partitions = []
@@ -454,11 +466,8 @@ class DataQualityTools:
     ) -> Optional[str]:
         """Get table DDL statement"""
         try:
-            query = (
-                f"SHOW CREATE TABLE {db_name}.{table_name}"
-                if db_name
-                else f"SHOW CREATE TABLE {table_name}"
-            )
+            safe_table_ref = build_table_reference(table_name, db_name)
+            query = f"SHOW CREATE TABLE {safe_table_ref}"
             auth_context = get_auth_context()
             result = await connection.execute(query, auth_context=auth_context)
             if result.data:
@@ -514,11 +523,17 @@ class DataQualityTools:
     
     async def _determine_optimized_sampling_strategy(self, connection, 
table_name: str, total_rows: int, sample_size: int) -> Dict[str, Any]:
         """Determine optimized sampling strategy"""
+        safe_sample_size = validate_integer(
+            sample_size,
+            "sample size",
+            minimum=0,
+            maximum=10_000_000,
+        )
         # Use thresholds from configuration
         small_threshold = self.config.data_quality.small_table_threshold
         medium_threshold = self.config.data_quality.medium_table_threshold
         
-        if total_rows <= sample_size or sample_size <= 0:
+        if total_rows <= safe_sample_size or safe_sample_size <= 0:
             return {
                 "sample_size": total_rows,
                 "sample_rate": 1.0,
@@ -527,7 +542,7 @@ class DataQualityTools:
                 "total_rows": total_rows
             }
         
-        sample_rate = sample_size / total_rows
+        sample_rate = safe_sample_size / total_rows
         
         # Stratified sampling strategy
         if total_rows <= small_threshold:
@@ -541,9 +556,13 @@ class DataQualityTools:
             }
         elif total_rows <= medium_threshold:
             # Medium table: simple LIMIT sampling (avoid ORDER BY RAND())
-            sample_table_expr = f"(SELECT * FROM {table_name} LIMIT 
{sample_size}) AS sample_table"
+            # SQL sink audit: table -> build_table_reference; sample size ->
+            # bounded integer; expression remains internal until execute.
+            sample_table_expr = (
+                f"(SELECT * FROM {table_name} LIMIT {safe_sample_size}) AS 
sample_table"  # nosec B608
+            )
             return {
-                "sample_size": sample_size,
+                "sample_size": safe_sample_size,
                 "sample_rate": sample_rate,
                 "sample_table_expression": sample_table_expr,
                 "sampling_method": "limit_sampling",
@@ -552,15 +571,23 @@ class DataQualityTools:
         else:
             # Large table: use simpler sampling strategy
             # For very large tables, still use LIMIT but increase sample size 
to improve representativeness
-            adjusted_sample_size = min(sample_size * 2, total_rows // 100)  # 
At most 1% sampling
-            sample_table_expr = f"(SELECT * FROM {table_name} LIMIT 
{adjusted_sample_size}) AS sample_table"
+            adjusted_sample_size = min(
+                safe_sample_size * 2,
+                total_rows // 100,
+            )  # At most 1% sampling
+            # SQL sink audit: table -> build_table_reference; adjusted sample
+            # size is derived from a bounded integer and DB row count before
+            # the expression reaches DorisConnection.execute.
+            sample_table_expr = (
+                f"(SELECT * FROM {table_name} LIMIT {adjusted_sample_size}) AS 
sample_table"  # nosec B608
+            )
             return {
                 "sample_size": adjusted_sample_size,
                 "sample_rate": adjusted_sample_size / total_rows,
                 "sample_table_expression": sample_table_expr,
                 "sampling_method": "enhanced_limit_sampling",
                 "total_rows": total_rows,
-                "original_sample_size": sample_size
+                "original_sample_size": safe_sample_size
             }
     
     async def _analyze_columns_batch(self, connection, table_name: str, 
columns_info: List[Dict], 
@@ -619,19 +646,29 @@ class DataQualityTools:
         try:
             # Build batch completeness query
             select_clauses = []
+            column_aliases = {}
             
             # First get total row count
             select_clauses.append("COUNT(*) as total_rows")
             
             # Then add statistics for each column
-            for col in columns_info:
+            for index, col in enumerate(columns_info):
                 col_name = col["column_name"]
+                safe_column = quote_identifier(col_name, "column name")
+                non_null_alias = f"column_{index}_non_null"
+                distinct_alias = f"column_{index}_distinct"
+                column_aliases[col_name] = (non_null_alias, distinct_alias)
                 select_clauses.extend([
-                    f"COUNT({col_name}) as {col_name}_non_null",
-                    f"COUNT(DISTINCT {col_name}) as {col_name}_distinct"
+                    f"COUNT({safe_column}) as {non_null_alias}",
+                    f"COUNT(DISTINCT {safe_column}) as {distinct_alias}"
                 ])
             
-            batch_sql = f"SELECT {', '.join(select_clauses)} FROM {table_expr}"
+            # SQL sink audit: metadata columns -> quote_identifier; aliases are
+            # index-derived; table_expr originates from a safe table/sampler
+            # before DorisConnection.execute.
+            batch_sql = (
+                f"SELECT {', '.join(select_clauses)} FROM {table_expr}"  # 
nosec B608
+            )
             
             auth_context = get_auth_context()
             result = await connection.execute(batch_sql, 
auth_context=auth_context)
@@ -644,8 +681,9 @@ class DataQualityTools:
             
             for col in columns_info:
                 col_name = col["column_name"]
-                non_null = row[f"{col_name}_non_null"]
-                distinct = row[f"{col_name}_distinct"]
+                non_null_alias, distinct_alias = column_aliases[col_name]
+                non_null = row[non_null_alias]
+                distinct = row[distinct_alias]
                 
                 null_count = total_rows - non_null
                 null_rate = null_count / total_rows if total_rows > 0 else 0
@@ -705,16 +743,30 @@ class DataQualityTools:
         """Batch numeric distribution analysis"""
         try:
             select_clauses = []
-            for col in numeric_columns:
+            column_aliases = {}
+            for index, col in enumerate(numeric_columns):
                 col_name = col["column_name"]
+                safe_column = quote_identifier(col_name, "column name")
+                aliases = {
+                    "min": f"column_{index}_min",
+                    "max": f"column_{index}_max",
+                    "avg": f"column_{index}_avg",
+                    "stddev": f"column_{index}_stddev",
+                }
+                column_aliases[col_name] = aliases
                 select_clauses.extend([
-                    f"MIN({col_name}) as {col_name}_min",
-                    f"MAX({col_name}) as {col_name}_max",
-                    f"AVG({col_name}) as {col_name}_avg",
-                    f"STDDEV({col_name}) as {col_name}_stddev"
+                    f"MIN({safe_column}) as {aliases['min']}",
+                    f"MAX({safe_column}) as {aliases['max']}",
+                    f"AVG({safe_column}) as {aliases['avg']}",
+                    f"STDDEV({safe_column}) as {aliases['stddev']}"
                 ])
             
-            batch_sql = f"SELECT {', '.join(select_clauses)} FROM {table_expr}"
+            # SQL sink audit: metadata columns -> quote_identifier; aliases are
+            # index-derived; table_expr originates from a safe table/sampler
+            # before DorisConnection.execute.
+            batch_sql = (
+                f"SELECT {', '.join(select_clauses)} FROM {table_expr}"  # 
nosec B608
+            )
             
             auth_context = get_auth_context()
             result = await connection.execute(batch_sql, 
auth_context=auth_context)
@@ -726,12 +778,17 @@ class DataQualityTools:
             
             for col in numeric_columns:
                 col_name = col["column_name"]
+                aliases = column_aliases[col_name]
                 numeric_results[col_name] = {
                     "data_type": "numeric",
-                    "min_value": row[f"{col_name}_min"],
-                    "max_value": row[f"{col_name}_max"],
-                    "mean": round(float(row[f"{col_name}_avg"]), 4) if 
row[f"{col_name}_avg"] is not None else None,
-                    "std_dev": round(float(row[f"{col_name}_stddev"]), 4) if 
row[f"{col_name}_stddev"] is not None else None
+                    "min_value": row[aliases["min"]],
+                    "max_value": row[aliases["max"]],
+                    "mean": round(float(row[aliases["avg"]]), 4)
+                    if row[aliases["avg"]] is not None
+                    else None,
+                    "std_dev": round(float(row[aliases["stddev"]]), 4)
+                    if row[aliases["stddev"]] is not None
+                    else None,
                 }
             
             return numeric_results
@@ -748,15 +805,19 @@ class DataQualityTools:
         for col in categorical_columns:
             col_name = col["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
                 # Get top 10 most frequent values
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 freq_sql = f"""
-                SELECT {col_name}, COUNT(*) as frequency
+                SELECT {safe_column}, COUNT(*) as frequency
                 FROM {table_expr}
-                WHERE {col_name} IS NOT NULL
-                GROUP BY {col_name}
+                WHERE {safe_column} IS NOT NULL
+                GROUP BY {safe_column}
                 ORDER BY frequency DESC
                 LIMIT 10
-                """
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 result = await connection.execute(freq_sql, 
auth_context=auth_context)
@@ -780,17 +841,29 @@ class DataQualityTools:
         """Batch temporal distribution analysis"""
         try:
             select_clauses = []
-            for col in temporal_columns:
+            column_aliases = {}
+            for index, col in enumerate(temporal_columns):
                 col_name = col["column_name"]
+                safe_column = quote_identifier(col_name, "column name")
+                aliases = {
+                    "min": f"column_{index}_min",
+                    "max": f"column_{index}_max",
+                }
+                column_aliases[col_name] = aliases
                 select_clauses.extend([
-                    f"MIN({col_name}) as {col_name}_min",
-                    f"MAX({col_name}) as {col_name}_max"
+                    f"MIN({safe_column}) as {aliases['min']}",
+                    f"MAX({safe_column}) as {aliases['max']}",
                 ])
             
             if not select_clauses:
                 return {}
             
-            batch_sql = f"SELECT {', '.join(select_clauses)} FROM {table_expr}"
+            # SQL sink audit: metadata columns -> quote_identifier; aliases are
+            # index-derived; table_expr originates from a safe table/sampler
+            # before DorisConnection.execute.
+            batch_sql = (
+                f"SELECT {', '.join(select_clauses)} FROM {table_expr}"  # 
nosec B608
+            )
             
             auth_context = get_auth_context()
             result = await connection.execute(batch_sql, 
auth_context=auth_context)
@@ -802,10 +875,11 @@ class DataQualityTools:
             
             for col in temporal_columns:
                 col_name = col["column_name"]
+                aliases = column_aliases[col_name]
                 temporal_results[col_name] = {
                     "data_type": "temporal",
-                    "min_value": row[f"{col_name}_min"],
-                    "max_value": row[f"{col_name}_max"]
+                    "min_value": row[aliases["min"]],
+                    "max_value": row[aliases["max"]],
                 }
             
             return temporal_results
@@ -826,14 +900,18 @@ class DataQualityTools:
             logger.info(f"  📊 [{i}/{len(columns_info)}] Analyzing column: 
{col_name}")
             
             try:
+                safe_column = quote_identifier(col_name, "column name")
                 # Completeness analysis SQL
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 completeness_sql = f"""
                 SELECT 
                     COUNT(*) as total_count,
-                    COUNT({col_name}) as non_null_count,
-                    COUNT(*) - COUNT({col_name}) as null_count
+                    COUNT({safe_column}) as non_null_count,
+                    COUNT(*) - COUNT({safe_column}) as null_count
                 FROM {table_expr}
-                """
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 result = await connection.execute(completeness_sql, 
auth_context=auth_context)
@@ -951,16 +1029,20 @@ class DataQualityTools:
         for column in numeric_columns:
             col_name = column["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 stats_sql = f"""
                 SELECT 
-                    COUNT({col_name}) as non_null_count,
-                    MIN({col_name}) as min_value,
-                    MAX({col_name}) as max_value,
-                    AVG({col_name}) as mean_value,
-                    STDDEV({col_name}) as std_dev
+                    COUNT({safe_column}) as non_null_count,
+                    MIN({safe_column}) as min_value,
+                    MAX({safe_column}) as max_value,
+                    AVG({safe_column}) as mean_value,
+                    STDDEV({safe_column}) as std_dev
                 FROM {table_expr}
-                WHERE {col_name} IS NOT NULL
-                """
+                WHERE {safe_column} IS NOT NULL
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 result = await connection.execute(stats_sql, 
auth_context=auth_context)
@@ -993,14 +1075,18 @@ class DataQualityTools:
         for column in categorical_columns:
             col_name = column["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
                 # Basic statistics
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 cardinality_sql = f"""
                 SELECT 
-                    COUNT(DISTINCT {col_name}) as cardinality,
-                    COUNT({col_name}) as non_null_count
+                    COUNT(DISTINCT {safe_column}) as cardinality,
+                    COUNT({safe_column}) as non_null_count
                 FROM {table_expr}
-                WHERE {col_name} IS NOT NULL
-                """
+                WHERE {safe_column} IS NOT NULL
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 cardinality_result = await connection.execute(cardinality_sql, 
auth_context=auth_context)
@@ -1018,14 +1104,17 @@ class DataQualityTools:
                     
                     # If cardinality is not too large, get distribution of top 
values
                     if cardinality <= 50 and detailed_response:
+                        # SQL sink audit: metadata column -> quote_identifier;
+                        # table_expr originates from a safe table/sampler 
before
+                        # DorisConnection.execute.
                         top_values_sql = f"""
-                        SELECT {col_name}, COUNT(*) as count
+                        SELECT {safe_column}, COUNT(*) as count
                         FROM {table_expr}
-                        WHERE {col_name} IS NOT NULL
-                        GROUP BY {col_name}
+                        WHERE {safe_column} IS NOT NULL
+                        GROUP BY {safe_column}
                         ORDER BY COUNT(*) DESC
                         LIMIT 10
-                        """
+                        """  # nosec B608
                         
                         top_values_result = await 
connection.execute(top_values_sql, auth_context=auth_context)
                         if top_values_result.data:
@@ -1047,14 +1136,18 @@ class DataQualityTools:
         for column in temporal_columns:
             col_name = column["column_name"]
             try:
+                safe_column = quote_identifier(col_name, "column name")
+                # SQL sink audit: metadata column -> quote_identifier;
+                # table_expr originates from a safe table/sampler before
+                # DorisConnection.execute.
                 stats_sql = f"""
                 SELECT 
-                    COUNT({col_name}) as non_null_count,
-                    MIN({col_name}) as min_date,
-                    MAX({col_name}) as max_date
+                    COUNT({safe_column}) as non_null_count,
+                    MIN({safe_column}) as min_date,
+                    MAX({safe_column}) as max_date
                 FROM {table_expr}
-                WHERE {col_name} IS NOT NULL
-                """
+                WHERE {safe_column} IS NOT NULL
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
                 result = await connection.execute(stats_sql, 
auth_context=auth_context)
diff --git a/doris_mcp_server/utils/dependency_analysis_tools.py 
b/doris_mcp_server/utils/dependency_analysis_tools.py
index feb55a3..34d6a95 100644
--- a/doris_mcp_server/utils/dependency_analysis_tools.py
+++ b/doris_mcp_server/utils/dependency_analysis_tools.py
@@ -151,6 +151,8 @@ class DependencyAnalysisTools:
             
             where_conditions.append(f"table_type IN ({','.join(table_types)})")
             
+            # SQL sink audit: database value -> bound param; table-type and
+            # WHERE fragments are fixed local variants -> 
DorisConnection.execute.
             metadata_sql = f"""
             SELECT 
                 table_schema as schema_name,
@@ -162,7 +164,7 @@ class DependencyAnalysisTools:
             FROM information_schema.tables
             WHERE {' AND '.join(where_conditions)}
             ORDER BY table_schema, table_name
-            """
+            """  # nosec B608
             
             # SECURITY FIX: Get auth_context and pass to execute for security 
validation
             auth_context = get_auth_context()
@@ -1022,4 +1024,4 @@ class DependencyAnalysisTools:
                     "action": "Review and implement suggested optimizations 
for better maintainability"
                 })
         
-        return recommendations 
\ No newline at end of file
+        return recommendations
diff --git a/doris_mcp_server/utils/performance_analytics_tools.py 
b/doris_mcp_server/utils/performance_analytics_tools.py
index a9e4a2a..1d15dec 100644
--- a/doris_mcp_server/utils/performance_analytics_tools.py
+++ b/doris_mcp_server/utils/performance_analytics_tools.py
@@ -30,6 +30,7 @@ from .logger import get_logger
 from .sql_security_utils import (
     SQLSecurityError,
     validate_identifier,
+    validate_integer,
     quote_identifier,
     build_table_reference,
     get_auth_context
@@ -65,6 +66,14 @@ class PerformanceAnalyticsTools:
             Slow query analysis results
         """
         try:
+            days = validate_integer(days, "days", minimum=1, maximum=3650)
+            top_n = validate_integer(top_n, "top N", minimum=1, maximum=1000)
+            min_execution_time_ms = validate_integer(
+                min_execution_time_ms,
+                "minimum execution time",
+                minimum=0,
+                maximum=86_400_000,
+            )
             start_time = time.time()
             connection = await self.connection_manager.get_connection("query")
             
@@ -140,6 +149,7 @@ class PerformanceAnalyticsTools:
             Resource growth analysis results
         """
         try:
+            days = validate_integer(days, "days", minimum=1, maximum=3650)
             start_time = time.time()
             connection = await self.connection_manager.get_connection("query")
             
@@ -220,7 +230,7 @@ class PerformanceAnalyticsTools:
             start_date = datetime.now() - timedelta(days=days)
             
             # Get daily query counts from audit logs
-            query_volume_sql = f"""
+            query_volume_sql = """
             SELECT 
                 DATE(`time`) as query_date,
                 COUNT(*) as total_queries,
@@ -229,7 +239,7 @@ class PerformanceAnalyticsTools:
                 SUM(`scan_bytes`) as total_scan_bytes,
                 SUM(`scan_rows`) as total_scan_rows
             FROM internal.__internal_schema.audit_log 
-            WHERE `time` >= '{start_date.strftime('%Y-%m-%d %H:%M:%S')}'
+            WHERE `time` >= %s
                 AND `stmt` IS NOT NULL
                 AND `stmt` != ''
             GROUP BY DATE(`time`)
@@ -237,7 +247,11 @@ class PerformanceAnalyticsTools:
             """
             
             auth_context = get_auth_context()
-            result = await connection.execute(query_volume_sql, 
auth_context=auth_context)
+            result = await connection.execute(
+                query_volume_sql,
+                params=(start_date,),
+                auth_context=auth_context,
+            )
             daily_data = result.data if result.data else []
             
             if not daily_data:
@@ -298,14 +312,14 @@ class PerformanceAnalyticsTools:
             start_date = datetime.now() - timedelta(days=days)
             
             # Get daily user activity from audit logs
-            user_activity_sql = f"""
+            user_activity_sql = """
             SELECT 
                 DATE(`time`) as activity_date,
                 COUNT(DISTINCT `user`) as daily_active_users,
                 COUNT(*) as total_queries,
                 COUNT(DISTINCT `client_ip`) as unique_ips
             FROM internal.__internal_schema.audit_log 
-            WHERE `time` >= '{start_date.strftime('%Y-%m-%d %H:%M:%S')}'
+            WHERE `time` >= %s
                 AND `stmt` IS NOT NULL
                 AND `stmt` != ''
             GROUP BY DATE(`time`)
@@ -313,7 +327,11 @@ class PerformanceAnalyticsTools:
             """
             
             auth_context = get_auth_context()
-            result = await connection.execute(user_activity_sql, 
auth_context=auth_context)
+            result = await connection.execute(
+                user_activity_sql,
+                params=(start_date,),
+                auth_context=auth_context,
+            )
             daily_data = result.data if result.data else []
             
             if not daily_data:
@@ -369,7 +387,7 @@ class PerformanceAnalyticsTools:
         try:
             start_date = datetime.now() - timedelta(days=days)
             
-            slow_query_sql = f"""
+            slow_query_sql = """
             SELECT 
                 `user` as user_name,
                 `client_ip` as host,
@@ -380,8 +398,8 @@ class PerformanceAnalyticsTools:
                 `scan_rows` as scan_rows,
                 `return_rows` as return_rows
             FROM internal.__internal_schema.audit_log 
-            WHERE `time` >= '{start_date.strftime('%Y-%m-%d %H:%M:%S')}'
-                AND `query_time` >= {min_execution_time_ms}
+            WHERE `time` >= %s
+                AND `query_time` >= %s
                 AND `stmt` IS NOT NULL
                 AND `stmt` != ''
                 AND `stmt` NOT LIKE '%__internal_schema%'
@@ -393,7 +411,11 @@ class PerformanceAnalyticsTools:
             """
             
             auth_context = get_auth_context()
-            result = await connection.execute(slow_query_sql, 
auth_context=auth_context)
+            result = await connection.execute(
+                slow_query_sql,
+                params=(start_date, min_execution_time_ms),
+                auth_context=auth_context,
+            )
             return result.data if result.data else []
             
         except Exception as e:
@@ -645,7 +667,17 @@ class PerformanceAnalyticsTools:
             for table_info in selected_tables["tables"]:
                 table_name = table_info["table_name"]
                 schema_name = table_info["schema_name"]
-                full_table_name = f"{schema_name}.{table_name}" if schema_name 
else table_name
+                try:
+                    full_table_name = build_table_reference(
+                        table_name,
+                        schema_name or None,
+                    )
+                except SQLSecurityError as exc:
+                    logger.warning(
+                        "Skipping table with unsafe metadata identifier: %s",
+                        exc,
+                    )
+                    continue
                 
                 logger.info(f"🔍 Analyzing table: {full_table_name} 
({table_info['size_mb']:.1f}MB)")
                 
@@ -1190,7 +1222,11 @@ class PerformanceAnalyticsTools:
             
             # If information_schema has no data, try COUNT query
             # full_table_name should already be validated by caller using 
build_table_reference
-            count_sql = f"SELECT COUNT(*) as rows FROM {full_table_name}"
+            # SQL sink audit: full_table_name is produced by
+            # build_table_reference before DorisConnection.execute.
+            count_sql = (
+                f"SELECT COUNT(*) as rows FROM {full_table_name}"  # nosec B608
+            )
             count_result = await connection.execute(count_sql, 
auth_context=auth_context)
             if count_result.data:
                 return {
@@ -1304,6 +1340,9 @@ class PerformanceAnalyticsTools:
             quoted_time_column = quote_identifier(time_column, "column name")
             
             # Aggregate data by date (full_table_name should be validated by 
caller)
+            # SQL sink audit: metadata column -> quote_identifier; table ->
+            # build_table_reference; days -> bound param before
+            # DorisConnection.execute.
             growth_sql = f"""
             SELECT 
                 DATE({quoted_time_column}) as date,
@@ -1314,7 +1353,7 @@ class PerformanceAnalyticsTools:
                 AND {quoted_time_column} IS NOT NULL
             GROUP BY DATE({quoted_time_column})
             ORDER BY date DESC
-            """
+            """  # nosec B608
             
             result = await connection.execute(growth_sql, params=(days,), 
auth_context=auth_context)
             if not result.data:
@@ -1807,4 +1846,4 @@ class PerformanceAnalyticsTools:
                     "action": "Review partitioning strategies and add 
appropriate indexes"
                 })
         
-        return recommendations 
\ No newline at end of file
+        return recommendations
diff --git a/doris_mcp_server/utils/schema_extractor.py 
b/doris_mcp_server/utils/schema_extractor.py
index e6da0bb..5454658 100644
--- a/doris_mcp_server/utils/schema_extractor.py
+++ b/doris_mcp_server/utils/schema_extractor.py
@@ -35,7 +35,8 @@ from .logger import get_logger
 from .sql_security_utils import (
     SQLSecurityError,
     validate_identifier,
-    quote_identifier
+    validate_integer,
+    quote_identifier,
 )
 
 # Configure logging
@@ -195,14 +196,19 @@ class MetadataExtractor:
 
         effective_db = db_name or self.db_name
         effective_catalog = catalog_name or self.catalog_name
-        query = f"""
+        query = """
         SELECT 1 AS TABLE_VISIBLE
         FROM information_schema.tables
-        WHERE TABLE_SCHEMA = '{effective_db}'
-          AND TABLE_NAME = '{table_name}'
+        WHERE TABLE_SCHEMA = %s
+          AND TABLE_NAME = %s
         LIMIT 1
         """
-        result = await self._execute_query_with_catalog_async(query, 
effective_db, effective_catalog)
+        result = await self._execute_query_with_catalog_async(
+            query,
+            effective_db,
+            effective_catalog,
+            params=(effective_db, table_name),
+        )
         if not result or not result[0]:
             raise self._doris_oauth_table_not_visible_error(table_name, 
effective_db, effective_catalog)
         
@@ -368,6 +374,14 @@ class MetadataExtractor:
         if not db_name:
             logger.warning("Database name not specified")
             return []
+
+        try:
+            validate_identifier(db_name, "database name")
+            if effective_catalog:
+                validate_identifier(effective_catalog, "catalog name")
+        except SQLSecurityError as exc:
+            logger.warning(f"Invalid identifier rejected: {exc}")
+            return []
         
         cache_key = f"tables_{effective_catalog or 'default'}_{db_name}"
         if cache_key in self.metadata_cache and (datetime.now() - 
self.metadata_cache_time.get(cache_key, datetime.min)).total_seconds() < 
self.cache_ttl:
@@ -375,6 +389,8 @@ class MetadataExtractor:
         
         try:
             # Use information_schema.tables table to get table list
+            # SQL sink audit: deprecated sync path validates both identifiers
+            # before the fixed metadata query reaches its legacy executor.
             query = f"""
             SELECT 
                 TABLE_NAME 
@@ -383,7 +399,7 @@ class MetadataExtractor:
             WHERE 
                 TABLE_SCHEMA = '{db_name}' 
                 AND TABLE_TYPE = 'BASE TABLE'
-            """
+            """  # nosec B608
             
             result = self._execute_query_with_catalog(query, db_name, 
effective_catalog)
             logger.info(
@@ -572,7 +588,7 @@ class MetadataExtractor:
         
         try:
             # Use information_schema.columns table to get table schema (async)
-            query = f"""
+            query = """
             SELECT 
                 COLUMN_NAME, 
                 DATA_TYPE, 
@@ -585,13 +601,18 @@ class MetadataExtractor:
             FROM 
                 information_schema.columns 
             WHERE 
-                TABLE_SCHEMA = '{db_name}' 
-                AND TABLE_NAME = '{table_name}'
+                TABLE_SCHEMA = %s
+                AND TABLE_NAME = %s
             ORDER BY 
                 ORDINAL_POSITION
             """
 
-            result = await self._execute_query_with_catalog_async(query, 
db_name, effective_catalog)
+            result = await self._execute_query_with_catalog_async(
+                query,
+                db_name,
+                effective_catalog,
+                params=(db_name, table_name),
+            )
 
             if not result:
                 logger.warning(f"Table {effective_catalog or 
'default'}.{db_name}.{table_name} does not exist or has no columns")
@@ -626,17 +647,20 @@ class MetadataExtractor:
 
             # Get table type information (async)
             try:
-                table_type_query = f"""
+                table_type_query = """
                 SELECT 
                     TABLE_TYPE,
                     ENGINE 
                 FROM 
                     information_schema.tables 
                 WHERE 
-                    TABLE_SCHEMA = '{db_name}' 
-                    AND TABLE_NAME = '{table_name}'
+                    TABLE_SCHEMA = %s
+                    AND TABLE_NAME = %s
                 """
-                table_type_result = await 
self._execute_query_async(table_type_query)
+                table_type_result = await self._execute_query_async(
+                    table_type_query,
+                    params=(db_name, table_name),
+                )
                 if table_type_result:
                     schema["table_type"] = 
table_type_result[0].get("TABLE_TYPE", "")
                     schema["engine"] = table_type_result[0].get("ENGINE", "")
@@ -687,6 +711,8 @@ class MetadataExtractor:
         
         try:
             # Use information_schema.tables table to get table comment
+            # SQL sink audit: deprecated sync path validates table, database
+            # and catalog identifiers before its legacy executor.
             query = f"""
             SELECT 
                 TABLE_COMMENT 
@@ -695,7 +721,7 @@ class MetadataExtractor:
             WHERE 
                 TABLE_SCHEMA = '{db_name}' 
                 AND TABLE_NAME = '{table_name}'
-            """
+            """  # nosec B608
             
             result = self._execute_query_with_catalog(query, db_name, 
effective_catalog)
             
@@ -748,6 +774,8 @@ class MetadataExtractor:
         
         try:
             # Use information_schema.columns table to get column comments
+            # SQL sink audit: deprecated sync path validates table, database
+            # and catalog identifiers before its legacy executor.
             query = f"""
             SELECT 
                 COLUMN_NAME, 
@@ -759,7 +787,7 @@ class MetadataExtractor:
                 AND TABLE_NAME = '{table_name}'
             ORDER BY 
                 ORDINAL_POSITION
-            """
+            """  # nosec B608
             
             result = self._execute_query_with_catalog(query, db_name, 
effective_catalog)
             
@@ -931,20 +959,6 @@ class MetadataExtractor:
             pd.DataFrame: Audit log DataFrame
         """
         try:
-            start_date = (datetime.now() - 
timedelta(days=days)).strftime('%Y-%m-%d')
-            
-            query = f"""
-            SELECT client_ip, user, db, time, stmt_id, stmt, state, error_code
-            FROM `__internal_schema`.`audit_log`
-            WHERE `time` >= '{start_date}'
-            AND state = 'EOF' AND error_code = 0
-            AND `stmt` NOT LIKE 'SHOW%'
-            AND `stmt` NOT LIKE 'DESC%'
-            AND `stmt` NOT LIKE 'EXPLAIN%'
-            AND `stmt` NOT LIKE 'SELECT 1%'
-            ORDER BY time DESC
-            LIMIT {limit}
-            """
             # Deprecated sync path removed; this method is deprecated overall
             df = pd.DataFrame()
             return df
@@ -1241,20 +1255,6 @@ class MetadataExtractor:
             Dict: Partition information
         """
         try:
-            # Get partition information
-            query = f"""
-            SELECT 
-                PARTITION_NAME,
-                PARTITION_EXPRESSION,
-                PARTITION_DESCRIPTION,
-                TABLE_ROWS
-            FROM 
-                information_schema.partitions
-            WHERE 
-                TABLE_SCHEMA = '{db_name}'
-                AND TABLE_NAME = '{table_name}'
-            """
-            
             # Deprecated sync path removed
             partitions = []
             
@@ -1281,7 +1281,13 @@ class MetadataExtractor:
 
     # Removed sync _execute_query_with_catalog; use async variant instead
 
-    async def _execute_query_with_catalog_async(self, query: str, db_name: str 
= None, catalog_name: str = None):
+    async def _execute_query_with_catalog_async(
+        self,
+        query: str,
+        db_name: str = None,
+        catalog_name: str = None,
+        params: tuple | None = None,
+    ):
         """
         Async version of _execute_query_with_catalog to avoid cross-event-loop 
issues.
 
@@ -1291,19 +1297,33 @@ class MetadataExtractor:
         """
         try:
             if catalog_name and 'information_schema' in query.lower():
-                modified_query = query.replace('information_schema', 
f'{catalog_name}.information_schema')
+                safe_catalog = quote_identifier(catalog_name, "catalog name")
+                modified_query = query.replace(
+                    'information_schema',
+                    f'{safe_catalog}.information_schema',
+                )
                 logger.info(
                     "Prepared catalog-qualified query for %s",
                     catalog_name,
                 )
-                return await self._execute_query_async(modified_query, db_name)
+                return await self._execute_query_async(
+                    modified_query,
+                    db_name,
+                    params=params,
+                )
             else:
-                return await self._execute_query_async(query, db_name)
+                return await self._execute_query_async(query, db_name, 
params=params)
         except Exception as e:
             logger.error(f"Error executing async query with catalog: {str(e)}")
             raise
 
-    async def _execute_query_async(self, query: str, db_name: str = None, 
return_dataframe: bool = False):
+    async def _execute_query_async(
+        self,
+        query: str,
+        db_name: str = None,
+        return_dataframe: bool = False,
+        params: tuple | None = None,
+    ):
         """
         Execute database query asynchronously
         
@@ -1319,7 +1339,12 @@ class MetadataExtractor:
         try:
             if self.connection_manager:
                 # Use the injected connection manager directly (async)
-                result = await 
self.connection_manager.execute_query(self._session_id, query, None, 
auth_context)
+                result = await self.connection_manager.execute_query(
+                    self._session_id,
+                    query,
+                    params,
+                    auth_context,
+                )
                 
                 # Extract data from QueryResult
                 if hasattr(result, 'data'):
@@ -1549,17 +1574,22 @@ class MetadataExtractor:
                 logger.warning(f"Invalid identifier rejected: {e}")
                 return ""
 
-            query = f"""
+            query = """
             SELECT 
                 TABLE_COMMENT 
             FROM 
                 information_schema.tables 
             WHERE 
-                TABLE_SCHEMA = '{effective_db}' 
-                AND TABLE_NAME = '{table_name}'
+                TABLE_SCHEMA = %s
+                AND TABLE_NAME = %s
             """
 
-            result = await self._execute_query_with_catalog_async(query, 
effective_db, effective_catalog)
+            result = await self._execute_query_with_catalog_async(
+                query,
+                effective_db,
+                effective_catalog,
+                params=(effective_db, table_name),
+            )
             if not result or not result[0]:
                 self._raise_if_doris_oauth_table_not_visible(table_name, 
effective_db, effective_catalog)
                 return ""
@@ -1584,20 +1614,25 @@ class MetadataExtractor:
                 logger.warning(f"Invalid identifier rejected: {e}")
                 return {}
 
-            query = f"""
+            query = """
             SELECT 
                 COLUMN_NAME, 
                 COLUMN_COMMENT 
             FROM 
                 information_schema.columns 
             WHERE 
-                TABLE_SCHEMA = '{effective_db}' 
-                AND TABLE_NAME = '{table_name}'
+                TABLE_SCHEMA = %s
+                AND TABLE_NAME = %s
             ORDER BY 
                 ORDINAL_POSITION
             """
 
-            rows = await self._execute_query_with_catalog_async(query, 
effective_db, effective_catalog)
+            rows = await self._execute_query_with_catalog_async(
+                query,
+                effective_db,
+                effective_catalog,
+                params=(effective_db, table_name),
+            )
             if not rows:
                 await self._ensure_table_visible_for_doris_oauth_metadata(
                     table_name,
@@ -1686,20 +1721,25 @@ class MetadataExtractor:
     async def get_recent_audit_logs_async(self, days: int = 7, limit: int = 
100):
         """Async version: get recent audit logs and return a pandas 
DataFrame."""
         try:
-            start_date = (datetime.now() - 
timedelta(days=days)).strftime('%Y-%m-%d')
-            query = f"""
+            safe_days = validate_integer(days, "days", minimum=1, maximum=3650)
+            safe_limit = validate_integer(limit, "limit", minimum=1, 
maximum=10000)
+            start_date = datetime.now() - timedelta(days=safe_days)
+            query = """
             SELECT client_ip, user, db, time, stmt_id, stmt, state, error_code
             FROM `__internal_schema`.`audit_log`
-            WHERE `time` >= '{start_date}'
+            WHERE `time` >= %s
             AND state = 'EOF' AND error_code = 0
             AND `stmt` NOT LIKE 'SHOW%'
             AND `stmt` NOT LIKE 'DESC%'
             AND `stmt` NOT LIKE 'EXPLAIN%'
             AND `stmt` NOT LIKE 'SELECT 1%'
             ORDER BY time DESC
-            LIMIT {limit}
+            LIMIT %s
             """
-            rows = await self._execute_query_async(query)
+            rows = await self._execute_query_async(
+                query,
+                params=(start_date, safe_limit),
+            )
             import pandas as pd
             return pd.DataFrame(rows or [])
         except Exception as e:
diff --git a/doris_mcp_server/utils/security_analytics_tools.py 
b/doris_mcp_server/utils/security_analytics_tools.py
index abbf78d..619fcdc 100644
--- a/doris_mcp_server/utils/security_analytics_tools.py
+++ b/doris_mcp_server/utils/security_analytics_tools.py
@@ -26,7 +26,7 @@ from collections import Counter, defaultdict
 
 from .db import DorisConnectionManager
 from .logger import get_logger
-from .sql_security_utils import get_auth_context
+from .sql_security_utils import get_auth_context, validate_integer
 
 logger = get_logger(__name__)
 
@@ -57,6 +57,13 @@ class SecurityAnalyticsTools:
         """
         connection = None
         try:
+            days = validate_integer(days, "days", minimum=1, maximum=3650)
+            min_query_threshold = validate_integer(
+                min_query_threshold,
+                "minimum query threshold",
+                minimum=0,
+                maximum=1_000_000,
+            )
             start_time = time.time()
             
             # 🚀 PROGRESS: Initialize security analysis
@@ -176,11 +183,15 @@ class SecurityAnalyticsTools:
         try:
             # System users filter
             system_user_filter = ""
+            params = [start_date, end_date]
             if not include_system_users:
                 system_users = ['root', 'admin', 'system', 'doris', 
'information_schema']
-                user_list = ','.join([f'"{user}"' for user in system_users])
-                system_user_filter = f"AND `user` NOT IN ({user_list})"
+                placeholders = ", ".join("%s" for _ in system_users)
+                system_user_filter = f"AND `user` NOT IN ({placeholders})"
+                params.extend(system_users)
             
+            # SQL sink audit: filter has only fixed local variants; date/user
+            # values are bound through params before DorisConnection.execute.
             audit_sql = f"""
             SELECT 
                 `user` as user_name,
@@ -193,24 +204,30 @@ class SecurityAnalyticsTools:
                 `return_rows` as return_rows,
                 `query_time` as execution_time_ms
             FROM internal.__internal_schema.audit_log 
-            WHERE `time` >= '{start_date.strftime('%Y-%m-%d %H:%M:%S')}'
-                AND `time` <= '{end_date.strftime('%Y-%m-%d %H:%M:%S')}'
+            WHERE `time` >= %s
+                AND `time` <= %s
                 AND `stmt` IS NOT NULL
                 AND `stmt` != ''
                 {system_user_filter}
             ORDER BY `time` DESC
             LIMIT 10000
-            """
+            """  # nosec B608
             
             # SECURITY FIX: Pass auth_context to execute
             auth_context = get_auth_context()
-            result = await connection.execute(audit_sql, 
auth_context=auth_context)
+            result = await connection.execute(
+                audit_sql,
+                params=tuple(params),
+                auth_context=auth_context,
+            )
             return result.data if result.data else []
             
         except Exception as e:
             logger.warning(f"Failed to get audit log data: {str(e)}")
             # Try alternative method without detailed metrics
             try:
+                # SQL sink audit: same fixed filter and bound values as the
+                # primary audit query.
                 simple_audit_sql = f"""
                 SELECT 
                     `user` as user_name,
@@ -219,16 +236,20 @@ class SecurityAnalyticsTools:
                     `stmt` as sql_statement,
                     `state` as query_status
                 FROM internal.__internal_schema.audit_log 
-                WHERE `time` >= '{start_date.strftime('%Y-%m-%d %H:%M:%S')}'
-                    AND `time` <= '{end_date.strftime('%Y-%m-%d %H:%M:%S')}'
+                WHERE `time` >= %s
+                    AND `time` <= %s
                     AND `stmt` IS NOT NULL
                     {system_user_filter}
                 ORDER BY `time` DESC
                 LIMIT 10000
-                """
+                """  # nosec B608
                 
                 auth_context = get_auth_context()
-                result = await connection.execute(simple_audit_sql, 
auth_context=auth_context)
+                result = await connection.execute(
+                    simple_audit_sql,
+                    params=tuple(params),
+                    auth_context=auth_context,
+                )
                 return result.data if result.data else []
                 
             except Exception as e2:
diff --git a/doris_mcp_server/utils/sql_security_utils.py 
b/doris_mcp_server/utils/sql_security_utils.py
index 5813daf..bddc27c 100644
--- a/doris_mcp_server/utils/sql_security_utils.py
+++ b/doris_mcp_server/utils/sql_security_utils.py
@@ -22,7 +22,8 @@ to prevent SQL injection attacks.
 """
 
 import re
-from typing import Optional, Tuple, List, Any
+from collections.abc import Mapping, Sequence
+from typing import Optional, Tuple
 
 from .logger import get_logger
 from .security import (
@@ -67,6 +68,19 @@ class SQLSecurityUtils:
         'DISTINCT', 'INTO', 'VALUES', 'SET', 'DEFAULT', 'PRIMARY', 'KEY',
         'FOREIGN', 'REFERENCES', 'CHECK', 'UNIQUE', 'CONSTRAINT'
     }
+
+    RULE_OPERATORS = {
+        "=",
+        "!=",
+        "<>",
+        "<",
+        ">",
+        "<=",
+        ">=",
+        "LIKE",
+        "NOT LIKE",
+    }
+    NULL_RULE_OPERATORS = {"IS NULL", "IS NOT NULL"}
     
     @classmethod
     def validate_identifier(cls, name: str, identifier_type: str = 
"identifier") -> str:
@@ -256,6 +270,73 @@ class SQLSecurityUtils:
             return f"{quoted_column} {operator} %s", True
         else:
             return f"{quoted_column} {operator}", False
+
+    @staticmethod
+    def validate_integer(
+        value: object,
+        value_name: str,
+        *,
+        minimum: int,
+        maximum: int,
+    ) -> int:
+        """Return an integer that is safe to embed in SQL grammar positions."""
+        if isinstance(value, bool) or not isinstance(value, int):
+            raise SQLSecurityError(f"Invalid {value_name}: must be an integer")
+        if not minimum <= value <= maximum:
+            raise SQLSecurityError(
+                f"Invalid {value_name}: must be between {minimum} and 
{maximum}"
+            )
+        return value
+
+    @classmethod
+    def build_rule_predicate(
+        cls,
+        rule: Mapping[str, object],
+    ) -> tuple[str, tuple[object, ...]]:
+        """Build one parameterized business-rule predicate.
+
+        Raw SQL fragments are intentionally unsupported. Callers provide a 
column,
+        an allowlisted operator, and a value (or values for ``IN``).
+        """
+        if "sql_condition" in rule:
+            raise SQLSecurityError(
+                "Raw sql_condition rules are not supported; use 
column/operator/value"
+            )
+
+        column_name = rule.get("column")
+        if not isinstance(column_name, str):
+            raise SQLSecurityError("Business rule column must be a string")
+        column = cls.quote_identifier(column_name, "column name")
+        operator = str(rule.get("operator", "=")).strip().upper()
+
+        if operator in cls.NULL_RULE_OPERATORS:
+            return f"{column} {operator}", ()
+
+        if operator == "IN":
+            values = rule.get("values")
+            if (
+                not isinstance(values, Sequence)
+                or isinstance(values, str | bytes)
+                or not values
+                or len(values) > 100
+            ):
+                raise SQLSecurityError(
+                    "IN rule values must be a non-empty sequence of at most 
100 items"
+                )
+            placeholders = ", ".join("%s" for _ in values)
+            return f"{column} IN ({placeholders})", tuple(values)
+
+        if operator not in cls.RULE_OPERATORS:
+            allowed = sorted(
+                cls.RULE_OPERATORS | cls.NULL_RULE_OPERATORS | {"IN"}
+            )
+            raise SQLSecurityError(
+                f"Invalid business rule operator: {operator!r}; allowed: 
{allowed}"
+            )
+
+        if "value" not in rule:
+            raise SQLSecurityError("Business rule value is required")
+        return f"{column} {operator} %s", (rule["value"],)
     
     @staticmethod
     def get_auth_context():
@@ -296,5 +377,7 @@ validate_identifier = SQLSecurityUtils.validate_identifier
 quote_identifier = SQLSecurityUtils.quote_identifier
 build_table_reference = SQLSecurityUtils.build_table_reference
 build_column_reference = SQLSecurityUtils.build_column_reference
+build_rule_predicate = SQLSecurityUtils.build_rule_predicate
+validate_integer = SQLSecurityUtils.validate_integer
 get_auth_context = SQLSecurityUtils.get_auth_context
 set_auth_context = SQLSecurityUtils.set_auth_context
diff --git a/test/integration/test_real_doris_transports.py 
b/test/integration/test_real_doris_transports.py
index df9099a..2f703fd 100644
--- a/test/integration/test_real_doris_transports.py
+++ b/test/integration/test_real_doris_transports.py
@@ -446,8 +446,68 @@ async def test_real_doris_tool_regression_paths(
         password=doris_sandbox.settings.password,
     )
     missing_table = f"{doris_sandbox.table}_missing"
+    with doris_sandbox.admin_connection.cursor() as cursor:
+        cursor.execute(
+            f"INSERT INTO {doris_sandbox.qualified_table} VALUES (1, %s)",
+            (doris_sandbox.marker,),
+        )
 
     async with _transport_client(transport, environment) as client:
+        basic_info_result = await client.call_tool(
+            "get_table_basic_info",
+            {
+                "table_name": doris_sandbox.table,
+                "db_name": doris_sandbox.settings.database,
+            },
+        )
+        assert basic_info_result.is_error is False
+        assert isinstance(basic_info_result.structured_content, dict)
+        assert basic_info_result.structured_content["row_count"] == 1
+        assert basic_info_result.structured_content["column_count"] == 2
+
+        column_analysis_result = await client.call_tool(
+            "analyze_columns",
+            {
+                "table_name": doris_sandbox.table,
+                "columns": ["id", "marker"],
+                "analysis_types": ["completeness"],
+                "sample_size": 10,
+                "db_name": doris_sandbox.settings.database,
+            },
+        )
+        assert column_analysis_result.is_error is False
+        assert isinstance(column_analysis_result.structured_content, dict)
+        assert column_analysis_result.structured_content["columns_analyzed"] 
== 2
+        assert set(
+            column_analysis_result.structured_content["completeness_analysis"]
+        ) == {"id", "marker"}
+
+        injected_identifier_result = await client.call_tool(
+            "analyze_columns",
+            {
+                "table_name": (
+                    f"{doris_sandbox.table}; "
+                    f"DROP TABLE {doris_sandbox.table}"
+                ),
+                "columns": ["id"],
+                "analysis_types": ["completeness"],
+                "sample_size": 10,
+                "db_name": doris_sandbox.settings.database,
+            },
+        )
+        assert injected_identifier_result.is_error is True
+        assert isinstance(injected_identifier_result.structured_content, dict)
+        assert "Invalid table name" in (
+            injected_identifier_result.structured_content["error"]
+        )
+
+        intact_result, intact_payload = await _exec_query(
+            client,
+            f"SELECT COUNT(*) AS row_count FROM 
{doris_sandbox.qualified_table}",
+        )
+        assert intact_result.is_error is False
+        assert intact_payload["data"] == [{"row_count": 1}]
+
         profile_result = await client.call_tool(
             "get_sql_profile",
             {
diff --git a/test/protocol/stdio_capability_server.py 
b/test/protocol/stdio_capability_server.py
index ad588fc..14adf19 100644
--- a/test/protocol/stdio_capability_server.py
+++ b/test/protocol/stdio_capability_server.py
@@ -20,6 +20,7 @@ import asyncio
 import json
 import logging
 import tempfile
+from contextlib import asynccontextmanager
 from types import SimpleNamespace
 
 from mcp.server.stdio import stdio_server
@@ -38,6 +39,7 @@ from doris_mcp_server.protocol import create_doris_mcp_server
 from doris_mcp_server.tools.tools_manager import DorisToolsManager
 from doris_mcp_server.utils.analysis_tools import SQLAnalyzer
 from doris_mcp_server.utils.data_governance_tools import DataGovernanceTools
+from doris_mcp_server.utils.data_quality_tools import DataQualityTools
 from doris_mcp_server.utils.db import QueryResult
 from doris_mcp_server.utils.security_analytics_tools import 
SecurityAnalyticsTools
 
@@ -83,6 +85,10 @@ class ProfileConnectionManager:
     async def get_connection(self, session_id: str) -> ProfileConnection:
         return self.connection
 
+    @asynccontextmanager
+    async def get_connection_context(self, session_id: str):
+        yield self.connection
+
 
 class ProfileAnalyzer(SQLAnalyzer):
     async def _get_query_id_by_trace_id(self, trace_id: str) -> str:
@@ -188,6 +194,15 @@ class OneToolManager:
         self.freshness_router.data_governance_tools = FreshnessGovernanceTools(
             connection_manager
         )
+        self.freshness_router.data_quality_tools = DataQualityTools(
+            connection_manager,
+            config=SimpleNamespace(
+                data_quality=SimpleNamespace(
+                    enable_batch_analysis=True,
+                    max_columns_per_batch=20,
+                )
+            ),
+        )
         self.role_analyzer = 
RoleSecurityAnalyticsTools(RoleConnectionManager())
 
     async def list_tools(self) -> list[Tool]:
@@ -228,6 +243,22 @@ class OneToolManager:
                 description="Exercise Doris 4 role metadata compatibility.",
                 input_schema={"type": "object", "properties": {}},
             ),
+            Tool(
+                name="analyze_columns",
+                description="Exercise production SQL identifier validation.",
+                input_schema={
+                    "type": "object",
+                    "properties": {
+                        "table_name": {"type": "string"},
+                        "columns": {
+                            "type": "array",
+                            "items": {"type": "string"},
+                        },
+                        "db_name": {"type": "string"},
+                    },
+                    "required": ["table_name", "columns"],
+                },
+            ),
         ]
 
     async def call_tool(self, name: str, arguments: dict) -> str:
@@ -257,6 +288,10 @@ class OneToolManager:
             return json.dumps(
                 await self.role_analyzer.analyze_data_access_patterns()
             )
+        if name == "analyze_columns":
+            return json.dumps(
+                await self.freshness_router._analyze_columns_tool(arguments)
+            )
         return "{}"
 
 
diff --git a/test/protocol/test_mcp_v2_protocol.py 
b/test/protocol/test_mcp_v2_protocol.py
index aa816e6..1bc3002 100644
--- a/test/protocol/test_mcp_v2_protocol.py
+++ b/test/protocol/test_mcp_v2_protocol.py
@@ -835,6 +835,53 @@ async def 
test_http_doris4_role_metadata_uses_public_grants_command():
         assert roles["operator"]["users"] == ["root"]
 
 
[email protected]
+async def test_http_rejects_injected_sql_identifier_and_recovers():
+    app = create_test_server(
+        tools_manager=ProfileToolManager(),
+    ).streamable_http_app(
+        json_response=True,
+        stateless_http=True,
+        host="127.0.0.1",
+        transport_security=create_transport_security("127.0.0.1"),
+    )
+
+    async with (
+        app.router.lifespan_context(app),
+        httpx2.ASGITransport(app) as transport,
+        httpx2.AsyncClient(
+            transport=transport,
+            base_url="http://127.0.0.1:3000";,
+        ) as client,
+    ):
+        rejected = await client.post(
+            "/mcp",
+            json=modern_tool_request(
+                1,
+                "analyze_columns",
+                {
+                    "table_name": "orders; DROP TABLE orders",
+                    "columns": ["id"],
+                    "db_name": "hhm_dt_sim",
+                },
+            ),
+            headers=modern_tool_headers("analyze_columns"),
+        )
+        assert rejected.status_code == 200
+        assert rejected.json()["result"]["isError"] is True
+        assert "Invalid table name" in (
+            rejected.json()["result"]["structuredContent"]["error"]
+        )
+
+        recovered = await client.post(
+            "/mcp",
+            json=modern_tool_request(2, "echo", {}),
+            headers=modern_tool_headers("echo"),
+        )
+        assert recovered.status_code == 200
+        assert recovered.json()["result"]["isError"] is False
+
+
 @pytest.mark.asyncio
 async def test_stdio_validates_capabilities_versions_and_process_survival():
     server_script = Path(__file__).with_name("stdio_capability_server.py")
@@ -886,6 +933,7 @@ async def 
test_stdio_validates_capabilities_versions_and_process_survival():
             "get_sql_profile",
             "monitor_data_freshness",
             "analyze_data_access_patterns",
+            "analyze_columns",
         ]
         secret = "stdio-secret-sec-016"
         error_result = await capable.call_tool(
@@ -912,6 +960,7 @@ async def 
test_stdio_validates_capabilities_versions_and_process_survival():
             "get_sql_profile",
             "monitor_data_freshness",
             "analyze_data_access_patterns",
+            "analyze_columns",
         ]
         legacy_error = await legacy.read_resource("doris://table/missing")
         assert (
@@ -1047,6 +1096,33 @@ async def 
test_stdio_doris4_role_metadata_uses_public_grants_command():
         assert list(roles) == ["operator"]
 
 
[email protected]
+async def test_stdio_rejects_injected_sql_identifier_and_recovers():
+    server_script = Path(__file__).with_name("stdio_capability_server.py")
+    server_params = StdioServerParameters(
+        command=sys.executable,
+        args=[str(server_script)],
+    )
+
+    async with Client(
+        stdio_client(server_params),
+        extensions=[advertise(REQUIRED_EXTENSION)],
+    ) as modern:
+        rejected = await modern.call_tool(
+            "analyze_columns",
+            {
+                "table_name": "orders; DROP TABLE orders",
+                "columns": ["id"],
+                "db_name": "hhm_dt_sim",
+            },
+        )
+        assert rejected.is_error is True
+        assert "Invalid table name" in rejected.structured_content["error"]
+
+        recovered = await modern.call_tool("echo", {})
+        assert recovered.is_error is False
+
+
 @pytest.mark.asyncio
 async def test_stdio_prompt_errors_are_typed_and_process_survives():
     server_script = Path(__file__).with_name("stdio_capability_server.py")
diff --git a/test/security/test_sql_sink_audit.py 
b/test/security/test_sql_sink_audit.py
new file mode 100644
index 0000000..a1c7b1d
--- /dev/null
+++ b/test/security/test_sql_sink_audit.py
@@ -0,0 +1,276 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import AsyncMock
+
+import pytest
+
+from doris_mcp_server.tools.resources_manager import DorisResourcesManager
+from doris_mcp_server.utils.analysis_tools import TableAnalyzer
+from doris_mcp_server.utils.data_governance_tools import DataGovernanceTools
+from doris_mcp_server.utils.data_quality_tools import DataQualityTools
+from doris_mcp_server.utils.performance_analytics_tools import (
+    PerformanceAnalyticsTools,
+)
+from doris_mcp_server.utils.schema_extractor import MetadataExtractor
+from doris_mcp_server.utils.security_analytics_tools import 
SecurityAnalyticsTools
+from doris_mcp_server.utils.sql_security_utils import (
+    SQLSecurityError,
+    build_rule_predicate,
+    validate_integer,
+)
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
+SOURCE_ROOT = REPOSITORY_ROOT / "doris_mcp_server"
+
+
+def test_b608_has_no_untriaged_dynamic_sql_findings():
+    bandit = shutil.which("bandit")
+    if bandit is None:
+        pytest.skip("install the project dev extra to run the Bandit SQL sink 
gate")
+    completed = subprocess.run(
+        [
+            bandit,
+            "-q",
+            "-r",
+            str(SOURCE_ROOT),
+            "-t",
+            "B608",
+            "-f",
+            "json",
+        ],
+        cwd=REPOSITORY_ROOT,
+        capture_output=True,
+        check=False,
+        text=True,
+    )
+
+    report = json.loads(completed.stdout)
+    assert report["results"] == [], completed.stdout
+    assert completed.returncode == 0
+
+
+def test_every_b608_exception_carries_local_source_validation_sink_rationale():
+    exceptions = 0
+    for path in SOURCE_ROOT.rglob("*.py"):
+        lines = path.read_text(encoding="utf-8").splitlines()
+        previous_exception = -1
+        for index, line in enumerate(lines):
+            if "# nosec B608" not in line:
+                continue
+            exceptions += 1
+            rationale = " ".join(
+                lines[max(previous_exception + 1, index - 50) : index + 1]
+            )
+            assert "SQL sink audit:" in rationale, f"missing rationale: 
{path}:{index + 1}"
+            assert (
+                "execute" in rationale or "executor" in rationale
+            ), f"missing sink: {path}:{index + 1}"
+            previous_exception = index
+
+    assert exceptions > 0
+
+
[email protected]("value", [True, "10", 1.5, None])
+def test_sql_integer_validation_rejects_non_integers(value):
+    with pytest.raises(SQLSecurityError):
+        validate_integer(value, "limit", minimum=0, maximum=100)
+
+
+def test_structured_business_rule_uses_bound_values():
+    attack_value = "active' OR 1=1 --"
+
+    predicate, params = build_rule_predicate(
+        {
+            "column": "status",
+            "operator": "=",
+            "value": attack_value,
+        }
+    )
+
+    assert predicate == "`status` = %s"
+    assert params == (attack_value,)
+    assert attack_value not in predicate
+
+
[email protected](
+    "rule",
+    [
+        {"sql_condition": "1=1); DROP TABLE users; --"},
+        {"sql_condition": ""},
+        {"operator": "=", "value": 1},
+        {"column": "status`; DROP TABLE users; --", "operator": "=", "value": 
1},
+        {"column": "status", "operator": "= 1 OR 1=1", "value": 1},
+        {"column": "status", "operator": "IN", "values": []},
+    ],
+)
+def test_structured_business_rule_rejects_raw_or_invalid_fragments(rule):
+    with pytest.raises(SQLSecurityError):
+        build_rule_predicate(rule)
+
+
[email protected]
+async def test_business_rule_sink_binds_untrusted_value():
+    attack_value = "active' OR 1=1 --"
+    connection = SimpleNamespace(
+        execute=AsyncMock(
+            return_value=SimpleNamespace(
+                data=[{"total_count": 2, "pass_count": 1}]
+            )
+        )
+    )
+    tools = DataGovernanceTools(SimpleNamespace())
+
+    result = await tools._check_business_rule_compliance(
+        connection,
+        "`internal`.`analytics`.`orders`",
+        [
+            {
+                "rule_name": "active-only",
+                "column": "status",
+                "operator": "=",
+                "value": attack_value,
+            }
+        ],
+        2,
+    )
+
+    assert result["active-only"]["pass_count"] == 1
+    sql = connection.execute.await_args.args[0]
+    assert attack_value not in sql
+    assert connection.execute.await_args.kwargs["params"] == (attack_value,)
+
+
[email protected]
+async def test_table_analyzer_rejects_identifier_injection_before_db_sink():
+    manager = SimpleNamespace(get_connection=AsyncMock())
+    analyzer = TableAnalyzer(manager)
+
+    result = await analyzer.analyze_column(
+        "orders`; DROP TABLE orders; --",
+        "amount",
+    )
+
+    assert result["success"] is False
+    manager.get_connection.assert_not_awaited()
+
+
[email protected]
+async def 
test_batch_column_analysis_quotes_metadata_identifiers_and_stable_aliases():
+    connection = SimpleNamespace(
+        execute=AsyncMock(
+            return_value=SimpleNamespace(
+                data=[
+                    {
+                        "total_rows": 2,
+                        "column_0_non_null": 1,
+                        "column_0_distinct": 1,
+                    }
+                ]
+            )
+        )
+    )
+    tools = object.__new__(DataQualityTools)
+
+    result = await tools._analyze_completeness_batch(
+        connection,
+        "`internal`.`analytics`.`orders`",
+        [{"column_name": "status"}],
+    )
+
+    sql = connection.execute.await_args.args[0]
+    assert "COUNT(`status`) as column_0_non_null" in sql
+    assert result["status"]["null_count"] == 1
+
+
+def test_resource_schema_filter_quotes_the_only_dynamic_identifier():
+    manager = DorisResourcesManager(SimpleNamespace())
+
+    condition, params = manager._schema_filter("table_schema", "analytics")
+
+    assert condition == "`table_schema` = %s"
+    assert params == ("analytics",)
+    with pytest.raises(SQLSecurityError):
+        manager._schema_filter("table_schema OR 1=1", "analytics")
+
+
[email protected]
+async def 
test_schema_metadata_values_are_bound_before_connection_manager_sink():
+    manager = SimpleNamespace(
+        execute_query=AsyncMock(
+            return_value=SimpleNamespace(data=[{"TABLE_VISIBLE": 1}])
+        )
+    )
+    extractor = MetadataExtractor(
+        db_name="analytics",
+        catalog_name="internal",
+        connection_manager=manager,
+    )
+    extractor._is_doris_oauth_context = lambda auth_context=None: True
+    attack_table = "orders' OR 1=1 --"
+    attack_database = "analytics' OR 1=1 --"
+
+    await extractor._ensure_table_visible_for_doris_oauth_metadata(
+        attack_table,
+        attack_database,
+        "internal",
+    )
+
+    sql = manager.execute_query.await_args.args[1]
+    params = manager.execute_query.await_args.args[2]
+    assert attack_table not in sql
+    assert attack_database not in sql
+    assert params == (attack_database, attack_table)
+
+
[email protected]
+async def test_audit_analytics_bind_dates_thresholds_and_system_users():
+    connection = SimpleNamespace(
+        execute=AsyncMock(return_value=SimpleNamespace(data=[]))
+    )
+    performance = PerformanceAnalyticsTools(SimpleNamespace())
+    security = SecurityAnalyticsTools(SimpleNamespace())
+
+    await performance._get_slow_query_data(connection, 7, 1000)
+    performance_sql = connection.execute.await_args.args[0]
+    performance_params = connection.execute.await_args.kwargs["params"]
+    assert "WHERE `time` >= %s" in performance_sql
+    assert "`query_time` >= %s" in performance_sql
+    assert performance_params[1] == 1000
+
+    connection.execute.reset_mock()
+    await security._get_audit_log_data(
+        connection,
+        SimpleNamespace(),
+        SimpleNamespace(),
+        include_system_users=False,
+    )
+    security_sql = connection.execute.await_args.args[0]
+    security_params = connection.execute.await_args.kwargs["params"]
+    assert "NOT IN (%s, %s, %s, %s, %s)" in security_sql
+    assert security_params[2:] == (
+        "root",
+        "admin",
+        "system",
+        "doris",
+        "information_schema",
+    )


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to