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


##########
superset/jinja_context.py:
##########
@@ -305,6 +311,67 @@ def url_param(
             self.cache_key_wrapper(result)
         return result
 
+    def get_guest_user_attribute(
+        self,
+        attribute_name: str,
+        default: JsonValue = None,
+        add_to_cache_keys: bool = True,
+        escape_result: bool = True,
+    ) -> JsonValue:
+        """
+        Get a specific user attribute from guest user.
+
+        This function retrieves attributes from the guest user token and 
supports
+        all JSON-native types (string, number, boolean, array, object, null).
+
+        Args:
+            attribute_name: Name of the attribute to retrieve
+            default: Default value if attribute not found (can be any 
JSON-native type)
+            add_to_cache_keys: Whether the value should be included in the 
cache key
+            escape_result: Escape string values (and strings inside lists) 
using
+                the database dialect's quoting so they are safe to interpolate
+                into SQL, mirroring ``url_param``. Enabled by default; 
non-string
+                JSON types are returned unchanged. Set to False for the raw 
value.
+
+        Returns:
+            The attribute value from the guest user token, or the default 
value.
+            Can be any JSON-native type: string, number, boolean, array, 
object, or
+            null.
+
+        Examples:
+            {{ get_guest_user_attribute('department') }}  # Returns: 
"Engineering"
+            {{ get_guest_user_attribute('is_admin') }}    # Returns: True
+            {{ get_guest_user_attribute('permissions') }} # Returns: ["read", 
"write"]
+            {{ get_guest_user_attribute('config') }}      # Returns: {"theme": 
"dark"}
+            {{ get_guest_user_attribute('missing', 'default') }} # Returns: 
"default"
+        """
+
+        # The macro only applies to guest users (embedded). is_guest_user()
+        # handles the feature-flag and request-context checks internally.
+        if not security_manager.is_guest_user():
+            return default
+
+        token = g.user.guest_token
+        user_attributes = token.get("user", {}).get("attributes") or {}
+
+        # Only add to the cache key if the attribute exists in the guest token
+        if attribute_name not in user_attributes:
+            return default

Review Comment:
   **Suggestion:** `get_guest_user_attribute` returns `default` immediately for 
non-guest users and missing attributes, which bypasses the method’s own 
escaping path even when `escape_result=True`. This can break SQL rendering (or 
allow injection) if the default contains quotes or comes from a non-escaped 
source. Route the default through the same escaping logic before returning so 
the method’s safety contract is consistent across all branches. [security]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   ❌ Jinja guest attribute macro bypasses SQL escaping for defaults.
   ⚠️ Embedded dashboards using default attributes risk unsafe SQL.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. SQL Lab and other templated queries render Jinja SQL via 
`_render_sql_template` in
   `superset/sql/execution/executor.py:13-29`, which calls
   `get_template_processor(database=self.database)` and then
   `BaseTemplateProcessor.process_template(sql, **template_params)` to evaluate 
macros in
   `superset/jinja_context.py`.
   
   2. `JinjaTemplateProcessor.set_context` registers the 
`get_guest_user_attribute` macro in
   the Jinja context at `superset/jinja_context.py:970-1021` by mapping
   `"get_guest_user_attribute"` to `partial(safe_proxy,
   extra_cache.get_guest_user_attribute)`, so it is available in any templated 
SQL executed
   through this pipeline.
   
   3. Inside `ExtraCache.get_guest_user_attribute` 
(`superset/jinja_context.py:314-373`), the
   method returns early for non-guest users and missing attributes: the branch 
`if not
   security_manager.is_guest_user(): return default` and the branch `if 
attribute_name not in
   user_attributes: return default` (lines 351-359) return the `default` 
argument directly
   without passing it through `_escape_value` or any dialect quoting.
   
   4. Execute a templated SQL query in SQL Lab such as `SELECT '{{
   get_guest_user_attribute("tenant", default_value) }}'` where `default_value` 
contains
   quotes or SQL fragments derived from another unescaped source; for a regular 
(non-guest)
   session `security_manager.is_guest_user()` is False, so the macro returns 
`default`
   unchanged, and the rendered SQL string includes this value verbatim, 
bypassing the
   dialect-quoting logic in `_escape_value` 
(`superset/jinja_context.py:416-435`) even though
   `escape_result=True`, breaking the macro's safety expectations and allowing 
unsafe content
   to flow into SQL.
   ```
   </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=f5558b1aff0548668f23bce652dc29ac&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=f5558b1aff0548668f23bce652dc29ac&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/jinja_context.py
   **Line:** 351:359
   **Comment:**
        *Security: `get_guest_user_attribute` returns `default` immediately for 
non-guest users and missing attributes, which bypasses the method’s own 
escaping path even when `escape_result=True`. This can break SQL rendering (or 
allow injection) if the default contains quotes or comes from a non-escaped 
source. Route the default through the same escaping logic before returning so 
the method’s safety contract is consistent across all branches.
   
   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%2F33924&comment_hash=7c24efd31b98534dafb202d91580c64462aab851c5ed7c2f042ee0c98e1ac0c3&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F33924&comment_hash=7c24efd31b98534dafb202d91580c64462aab851c5ed7c2f042ee0c98e1ac0c3&reaction=dislike'>👎</a>



##########
superset/jinja_context.py:
##########
@@ -305,6 +311,67 @@ def url_param(
             self.cache_key_wrapper(result)
         return result
 
+    def get_guest_user_attribute(
+        self,
+        attribute_name: str,
+        default: JsonValue = None,
+        add_to_cache_keys: bool = True,
+        escape_result: bool = True,
+    ) -> JsonValue:
+        """
+        Get a specific user attribute from guest user.
+
+        This function retrieves attributes from the guest user token and 
supports
+        all JSON-native types (string, number, boolean, array, object, null).
+
+        Args:
+            attribute_name: Name of the attribute to retrieve
+            default: Default value if attribute not found (can be any 
JSON-native type)
+            add_to_cache_keys: Whether the value should be included in the 
cache key
+            escape_result: Escape string values (and strings inside lists) 
using
+                the database dialect's quoting so they are safe to interpolate
+                into SQL, mirroring ``url_param``. Enabled by default; 
non-string
+                JSON types are returned unchanged. Set to False for the raw 
value.
+
+        Returns:
+            The attribute value from the guest user token, or the default 
value.
+            Can be any JSON-native type: string, number, boolean, array, 
object, or
+            null.
+
+        Examples:
+            {{ get_guest_user_attribute('department') }}  # Returns: 
"Engineering"
+            {{ get_guest_user_attribute('is_admin') }}    # Returns: True
+            {{ get_guest_user_attribute('permissions') }} # Returns: ["read", 
"write"]
+            {{ get_guest_user_attribute('config') }}      # Returns: {"theme": 
"dark"}
+            {{ get_guest_user_attribute('missing', 'default') }} # Returns: 
"default"
+        """
+
+        # The macro only applies to guest users (embedded). is_guest_user()
+        # handles the feature-flag and request-context checks internally.
+        if not security_manager.is_guest_user():
+            return default
+
+        token = g.user.guest_token
+        user_attributes = token.get("user", {}).get("attributes") or {}
+
+        # Only add to the cache key if the attribute exists in the guest token
+        if attribute_name not in user_attributes:
+            return default
+
+        result = user_attributes[attribute_name]
+        if add_to_cache_keys and result is not None:
+            # Use json.dumps for consistent serialization of all JSON-native 
types
+            cache_value = json.dumps(result, sort_keys=True)
+            self.cache_key_wrapper(
+                f"guest_user_attribute:{attribute_name}:{cache_value}"
+            )
+        # Guest attributes are interpolated into the rendered SQL, so escape
+        # string values (and strings within lists) with the dialect's quoting
+        # by default, mirroring url_param. Non-string JSON types pass through.
+        if escape_result:
+            result = self._escape_value(result)
+        return result

Review Comment:
   **Suggestion:** The new macro advertises JSON object support, but escaping 
only delegates to `_escape_value`, which does not escape strings nested inside 
dictionaries. That leaves nested attribute strings unescaped when templates 
access object members (for example, `get_guest_user_attribute(...)[...]`) and 
interpolate them into SQL. Either recursively escape string values in dict 
structures or restrict this macro to scalar/list string-safe outputs when 
`escape_result=True`. [security]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   ❌ Nested JSON guest attributes can inject unescaped strings into SQL.
   ⚠️ Multi-tenant dashboards using JSON attributes risk query compromise.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Guest embedded dashboards and other guest user queries are rendered via 
the same Jinja
   pipeline as SQL Lab: `_render_sql_template` in 
`superset/sql/execution/executor.py:13-29`
   calls `get_template_processor`, which instantiates a 
`JinjaTemplateProcessor` and injects
   `get_guest_user_attribute` into the context 
(`superset/jinja_context.py:970-1021`).
   
   2. For guest users, `ExtraCache.get_guest_user_attribute` at
   `superset/jinja_context.py:314-373` loads the guest token from 
`g.user.guest_token`, reads
   `user_attributes = token.get("user", {}).get("attributes") or {}` (lines 
354-355), and if
   the attribute exists assigns `result = user_attributes[attribute_name]` 
(line 361).
   
   3. The method then applies escaping only via `_escape_value` when 
`escape_result` is True
   (lines 368-372), but `_escape_value` (`superset/jinja_context.py:416-435`) 
explicitly
   escapes only strings and lists of strings, returning dictionaries unchanged; 
thus a JSON
   object attribute like `{"tenant": {"id": "foo' OR 1=1 --"}}` is returned as 
a dict without
   any nested string escaping even though `escape_result=True`.
   
   4. A SQL template that uses an object attribute and indexes into it (for 
example, `WHERE
   tenant_id = '{{ get_guest_user_attribute("tenant")["id"] }}'`) causes
   `get_guest_user_attribute` to return the dict unchanged; Jinja then 
evaluates `["id"]` to
   a raw string, which is interpolated directly into the SQL without going 
through dialect
   quoting, so malicious or malformed nested strings in JSON attributes can 
reach the final
   SQL despite the macro being invoked with escaping enabled.
   ```
   </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=28c8303a38cb4b28bd50c1f5a0e6ed59&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=28c8303a38cb4b28bd50c1f5a0e6ed59&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/jinja_context.py
   **Line:** 368:373
   **Comment:**
        *Security: The new macro advertises JSON object support, but escaping 
only delegates to `_escape_value`, which does not escape strings nested inside 
dictionaries. That leaves nested attribute strings unescaped when templates 
access object members (for example, `get_guest_user_attribute(...)[...]`) and 
interpolate them into SQL. Either recursively escape string values in dict 
structures or restrict this macro to scalar/list string-safe outputs when 
`escape_result=True`.
   
   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%2F33924&comment_hash=cbc031ebe16dafa6dc711492c36a876c260a00abe453e1a7ffd219e136521a28&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F33924&comment_hash=cbc031ebe16dafa6dc711492c36a876c260a00abe453e1a7ffd219e136521a28&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