codeant-ai-for-open-source[bot] commented on code in PR #41120:
URL: https://github.com/apache/superset/pull/41120#discussion_r3507283243


##########
superset/commands/sql_lab/estimate.py:
##########
@@ -102,57 +101,43 @@ def _apply_sql_security(self, sql: str) -> str:
             db_engine_spec.engine,
             set(),
         )
-        if disallowed_tables and 
parsed_script.check_tables_present(disallowed_tables):
-            found_tables = set()
-            for statement in parsed_script.statements:
-                present = {table.table.lower() for table in statement.tables}
-                for table in disallowed_tables:
-                    if table.lower() in present:
-                        found_tables.add(table)
-            raise SupersetDisallowedSQLTableException(found_tables or 
disallowed_tables)
+        rls_enabled = is_feature_enabled("RLS_IN_SQLLAB")
+
+        # Resolve the effective per-query schema once, the same way the 
execution
+        # path does (``sql_lab.execute_sql_statements``), but only when a 
control
+        # below actually needs it. Going through 
``get_default_schema_for_query``
+        # rather than the static ``get_default_schema`` runs engine-specific
+        # per-query security gates too — e.g. ``PostgresEngineSpec`` rejects a
+        # query that sets ``search_path`` — and resolves unqualified 
references to
+        # the schema the engine uses at runtime, so both the denylist check and
+        # RLS injection match the execution path exactly.
+        catalog: str | None = None
+        effective_schema = ""
+        if disallowed_tables or rls_enabled:
+            catalog = self._catalog or self._database.get_default_catalog()
+            resolved_schema = self._database.resolve_query_default_schema(
+                self._sql, self._schema, catalog, self._template_params
+            )

Review Comment:
   **Suggestion:** This path resolves the effective schema from `self._sql` 
even though `_apply_sql_security` is validating the `sql` argument. When the 
argument has already been rendered/transformed, the schema/security gate may 
inspect different SQL than the denylist parser, producing inconsistent 
allow/deny behavior. Resolve the default schema from the same `sql` value being 
checked in this method. [security]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ SQL Lab cost estimate denylists may misfire under templating.
   - ⚠️ RLS predicates may target wrong schema during estimation.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Trigger SQL Lab cost estimation, which calls QueryEstimationCommand.run() 
in
   superset/commands/sql_lab/estimate.py:76-100 (see BulkRead output where 
run() starts
   around file line 145).
   
   2. In run(), observe that `sql` is first set from `self._sql` and then, when
   `self._template_params` is present, rendered via 
`template_processor.process_template(sql,
   **self._template_params)` before being passed to 
`self._apply_sql_security(sql)`
   (estimate.py:81-90).
   
   3. Inside `_apply_sql_security` (superset/commands/sql_lab/estimate.py:80-74 
relative to
   file, shown at lines 11-74 in BulkRead), note that the denylist and RLS 
checks parse the
   *rendered* SQL argument via `parsed_script = SQLScript(sql, 
engine=db_engine_spec.engine)`
   (line ~20 in the excerpt).
   
   4. In the same method, the effective schema used for denylist matching and 
RLS injection
   is resolved via `resolved_schema = 
self._database.resolve_query_default_schema(self._sql,
   self._schema, catalog, self._template_params)` (estimate.py:118-120), which 
builds a probe
   Query using the original `self._sql` and re-renders templates inside
   `get_default_schema_for_query` (superset/models/core.py:701-707). Because
   `_apply_sql_security` validates the already-rendered `sql` argument while 
schema
   resolution runs against a separate render of `self._sql`, any 
non-deterministic or
   context-dependent templating (e.g. macros that vary per render) can make the
   schema/security gate inspect different SQL than the denylist parser, causing 
inconsistent
   allow/deny behavior for table denylists and RLS in the cost-estimation path.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=407d8cb52af04bf88970ec2f676bff42&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=407d8cb52af04bf88970ec2f676bff42&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/commands/sql_lab/estimate.py
   **Line:** 118:120
   **Comment:**
        *Security: This path resolves the effective schema from `self._sql` 
even though `_apply_sql_security` is validating the `sql` argument. When the 
argument has already been rendered/transformed, the schema/security gate may 
inspect different SQL than the denylist parser, producing inconsistent 
allow/deny behavior. Resolve the default schema from the same `sql` value being 
checked in this method.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41120&comment_hash=b0b3431cacfee81b615954af53c11cc5c59594def5943bff75aa39f583dcd298&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41120&comment_hash=b0b3431cacfee81b615954af53c11cc5c59594def5943bff75aa39f583dcd298&reaction=dislike'>👎</a>



##########
superset/sql/parse.py:
##########
@@ -1060,15 +1057,135 @@ def check_functions_present(self, functions: set[str]) 
-> bool:
 
         return any(function.upper() in present for function in functions)
 
-    def check_tables_present(self, tables: set[str]) -> bool:
+    def check_tables_present(
+        self, tables: set[str], default_schema: str | None = None
+    ) -> bool:
         """
         Check if any of the given tables are present in the statement.
 
+        Denylist entries may be bare (``pg_stat_activity``) or
+        schema-qualified (``information_schema.tables``). Bare entries
+        match by table name regardless of schema; qualified entries
+        require the schema to match too. This lets us block all access
+        to ``information_schema`` without also blocking any
+        user-authored table that happens to be named ``tables``.
+
         :param tables: Set of table names to check for (case-insensitive)
-        :return: True if any of the tables are present
+        :param default_schema: Schema unqualified references resolve to at
+            runtime (e.g. the session ``search_path`` / selected schema)
+        :return: True if any of the given tables is referenced
+        """
+        return bool(self.get_disallowed_tables(tables, default_schema))
+
+    def changes_search_path(self) -> bool:
+        """
+        Return True if the statement changes the session ``search_path``.
+
+        A ``SET search_path = ...`` makes unqualified references in later
+        statements resolve to a schema other than the caller's
+        ``default_schema``, so denylist matching against ``default_schema``
+        alone becomes unreliable once such a statement is present.
+        """
+        # `SET search_path = schema` (and the `TO`/`SESSION`/`LOCAL` variants)
+        # parse as a structured exp.Set, surfaced by get_settings(). Strip any
+        # identifier quoting so `SET "search_path" = ...` (equivalent to the
+        # unquoted form in Postgres) is still recognized.
+        if any(key.strip('"').lower() == "search_path" for key in 
self.get_settings()):
+            return True
+        # `set_config('search_path', ...)` rebinds the search path through a
+        # function call rather than a SET statement, so it never reaches
+        # get_settings() and must be detected on the parsed tree.
+        for func in self._parsed.find_all(exp.Anonymous):
+            if (
+                func.name.lower() == "set_config"
+                and func.expressions
+                and isinstance(func.expressions[0], exp.Literal)
+                and func.expressions[0].name.lower() == "search_path"
+            ):
+                return True
+        # Exotic forms (e.g. `SET search_path TO "$user", public`) fall back to
+        # an opaque exp.Command. Match the leading setting name rather than
+        # scanning the whole expression, so `SET ROLE my_search_path_role`
+        # (whose value merely contains the substring) is not misclassified.
+        parsed = self._parsed
+        if isinstance(parsed, exp.Command) and parsed.name.upper() == "SET":
+            tokens = str(parsed.expression).replace("=", " ").split()
+            while tokens and tokens[0].upper() in {"SESSION", "LOCAL"}:
+                tokens.pop(0)
+            return bool(tokens) and tokens[0].strip('"').lower() == 
"search_path"
+        return False

Review Comment:
   **Suggestion:** The search-path change detector only handles 
`SET`/`set_config` forms and misses `RESET search_path`, which also changes how 
later unqualified tables resolve. In multi-statement scripts this can leave 
`schema_indeterminate` false and allow a schema-qualified denylist entry to be 
bypassed after a reset. Extend `changes_search_path()` to treat `RESET 
search_path` as a search-path mutation. [security]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ RESET search_path lets queries evade information_schema table denylists.
   - ⚠️ Multi-statement SQLLab queries may bypass schema-qualified protections.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Configure DISALLOWED_SQL_TABLES for a PostgreSQL engine to include a 
schema-qualified
   entry such as `information_schema.tables`, as used by SQL Lab and the 
executor paths (see
   usage in superset/sql_lab.py:452-456 and 
superset/sql/execution/executor.py:736-758 where
   `parsed_script.get_disallowed_tables(...)` is called).
   
   2. Submit a multi-statement script against that database via SQL Lab or
   Database.execute(), for example `RESET search_path; SELECT * FROM tables;`, 
where
   PostgreSQL resolves unqualified `tables` to `information_schema.tables` 
after the `RESET
   search_path` statement based on the server’s default search_path.
   
   3. Observe that the script is parsed into a SQLScript and checked for 
denylists using
   `SQLScript.get_disallowed_tables(disallowed_tables, default_schema)`
   (superset/sql/parse.py:1833-1859). This method iterates statements, passing a
   `schema_indeterminate` flag that is flipped when 
`statement.changes_search_path()` returns
   True, so later statements will conservatively match unqualified references 
against
   schema-qualified denylist entries.
   
   4. Inspect `SQLStatement.changes_search_path()` 
(superset/sql/parse.py:81-117). It returns
   True for explicit `SET search_path` forms (via `get_settings()` and exp.Set) 
and for
   `set_config('search_path', ...)`, and finally for opaque `SET ...` commands 
by checking
   `parsed = self._parsed` and `isinstance(parsed, exp.Command) and 
parsed.name.upper() ==
   "SET"` with a tokenized setting name. There is no handling for `RESET 
search_path`, which
   SQLGlot parses as `exp.Command` named `"RESET"`, so `changes_search_path()` 
returns False
   for that statement. As a result, `schema_indeterminate` never becomes True, 
and the
   following `SELECT * FROM tables` is matched in 
`SQLStatement.get_disallowed_tables()` only
   against `default_schema.tables`, not against the schema-qualified
   `information_schema.tables` denylist entry. The script therefore passes 
denylist checks
   while at runtime the unqualified `tables` actually resolves to the blocked
   `information_schema.tables`, allowing `RESET search_path` to bypass 
schema-qualified table
   denylists in multi-statement scripts.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=6e1340d7a05f47c2aba51576603ed9e9&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=6e1340d7a05f47c2aba51576603ed9e9&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/sql/parse.py
   **Line:** 1110:1116
   **Comment:**
        *Security: The search-path change detector only handles 
`SET`/`set_config` forms and misses `RESET search_path`, which also changes how 
later unqualified tables resolve. In multi-statement scripts this can leave 
`schema_indeterminate` false and allow a schema-qualified denylist entry to be 
bypassed after a reset. Extend `changes_search_path()` to treat `RESET 
search_path` as a search-path mutation.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41120&comment_hash=da543ff774f66013a34bba7bcda6b7db245818fc4e4c4309aef44306fa8454a8&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41120&comment_hash=da543ff774f66013a34bba7bcda6b7db245818fc4e4c4309aef44306fa8454a8&reaction=dislike'>👎</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to