Bug description

When trying to embed Superset dashboard, I discovered, that with Talisman enabled, /embedded endpoint produces X-Frame-Options: SAMEORIGIN - which does not allow to embed dashboards in pages of different origin. This seems to be too limiting - while I'm fine with other endpoints having this policy, this particular one is specially designated for embedding, so it makes sense for it do allow wider permissions. I wonder if this would be a better option:


--- a/superset/embedded/view.py
+++ b/superset/embedded/view.py
@@ -27,6 +27,7 @@ from superset.daos.dashboard import EmbeddedDashboardDAO
 from superset.superset_typing import FlaskResponse
 from superset.utils import core as utils
 from superset.views.base import BaseSupersetView, common_bootstrap_payload
+from superset.extensions import talisman


 class EmbeddedView(BaseSupersetView):
@@ -34,6 +35,7 @@ class EmbeddedView(BaseSupersetView):

     route_base = "/embedded"

+    @talisman(frame_options=None)
     @expose("/<uuid>")
     @event_logger.log_this_with_extra_payload
     def embedded(

How to reproduce the bug

  1. Make embeddable dashboard
  2. Try to embed it from another domain
  3. See something like: Refused to display 'https://mysite.embedding/' in a frame because it set 'X-Frame-Options' to 'sameorigin'. in the browser console.

Screenshots/recordings

No response

Superset version

3.0.4

Python version

3.9

Node version

16

Browser

Chrome

Additional context

No response

Checklist

  • [X] I have searched Superset docs and Slack and didn't find a solution to my problem.
  • [X] I have searched the GitHub issue tracker and didn't find a similar bug report.
  • [X] I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.

Comment From: michael-s-molina

Hi @smalyshev. According to ChatGPT:

It is possible to override the X-Frame-Options header with a CSP (Content Security Policy) directive. This can be done by using the frame-ancestors directive in the CSP header. By specifying the desired domains in the frame-ancestors directive, you can allow the content to be loaded in an iframe from those domains.

Here is an example of how to override the X-Frame-Options header using the frame-ancestors directive in a CSP header:

content-security-policy: frame-ancestors https://example.com https://anotherdomain.com;

In this example, the content is allowed to be loaded in iframes from https://example.com and https://anotherdomain.com.

You can find more information about CSP and its directives in the Security Docs — Content Security Policy (CSP) article.

To override Superset's CSP policy use TALISMAN_CONFIG.

Comment From: smalyshev

Thank you for the answer. I understand that CSP policy can be amended in TALISMAN_CONFIG, but changing this config would affect all the endpoints. What I am suggesting is leaving the policy as is for all the endpoints - because it's mostly appropriate that parts of the application that are not meant for embedding will not be embeddable - and only changing the policy in one specific endpoint that is meant for embedding. Is this possible to do by modifying TALISMAN_CONFIG?

Comment From: michael-s-molina

Thank you for the answer. I understand that CSP policy can be amended in TALISMAN_CONFIG, but changing this config would affect all the endpoints. What I am suggesting is leaving the policy as is for all the endpoints

To be able to do this, we would need a specific configuration for this endpoint given that Frame-Options: None is not a secure configuration. Considering that in terms of security, one door is sufficient for an attack, I'm not sure about the benefit of restricting other endpoints given that you already opened the door for the embedded endpoint. I'm assuming that when you do that, you'll configure a trusted domain instead of any.

Comment From: smalyshev

I'm not sure what specific attack you're talking about, but in terms of framing I am assuming I do not want my whole superset app to be displayed in a frame on a separate site, but I do want a specific diagram, that I explicitly designated as shareable, and that uses special role & guest token for accessing it, to be accessible in a frame. I think it is different from security standpoint whether we allow it for the whole application or for one specific endpoint that is designed for sharing and embedding. I am not insisting on removing all framing options - making it configurable would be ok too, what I am looking for is to be able to set more relaxed settings for the specifically embeddable endpoint than for the rest of the application, which I don't want to make embeddable as a whole.

Comment From: michael-s-molina

Tagging @dpgaspar @jfrag1 @eschutho @kgabryje who have more context on embedded and might help figuring out a secure solution for your problem.

Comment From: rusackas

Anyone still looking for an answer (or willing to provide one) here? This thread has been silent for long enough that it's at risk of being closed as stale. In the meantime, perhaps @dosu-bot can help.

Comment From: dosubot[bot]

Hello, @rusackas! I'm here to help you out while we wait for a human maintainer. I'm a bot that can assist you with bug-solving, answering questions, and becoming a contributor. Let me know how I can assist you!

To set more relaxed X-Frame-Options settings for the /embedded endpoint in Superset without affecting the rest of the application, you can use the @talisman decorator to override the default settings for this specific view. Here is how you can modify the embedded method in EmbeddedView to achieve this:

from flask_talisman import talisman

class EmbeddedView(BaseSupersetView):
    """The views for embedded resources to be rendered in an iframe"""

    route_base = "/embedded"

    @expose("/<uuid>")
    @event_logger.log_this_with_extra_payload
    @talisman(frame_options_allow_from="*")  # Set more relaxed X-Frame-Options
    def embedded(
        self,
        uuid: str,
        add_extra_log_payload: Callable[..., None] = lambda **kwargs: None,
    ) -> FlaskResponse:
        """
        Server side rendering for the embedded dashboard page
        :param uuid: identifier for embedded dashboard
        :param add_extra_log_payload: added by `log_this_with_manual_updates`, set a
            default value to appease pylint
        """
        if not is_feature_enabled("EMBEDDED_SUPERSET"):
            abort(404)

        embedded = EmbeddedDashboardDAO.find_by_id(uuid)

        if not embedded:
            abort(404)

        assert embedded is not None

        # validate request referrer in allowed domains
        is_referrer_allowed = not embedded.allowed_domains
        for domain in embedded.allowed_domains:
            if same_origin(request.referrer, domain):
                is_referrer_allowed = True
                break

        if not is_referrer_allowed:
            abort(403)

        # Log in as an anonymous user, just for this view.
        # This view needs to be visible to all users,
        # and building the page fails if g.user and/or ctx.user aren't present.
        login_user(AnonymousUserMixin(), force=True)

        add_extra_log_payload(
            embedded_dashboard_id=uuid,
            dashboard_version="v2",
        )

        bootstrap_data = {
            "config": {
                "GUEST_TOKEN_HEADER_NAME": current_app.config["GUEST_TOKEN_HEADER_NAME"]
            },
            "common": common_bootstrap_payload(),
            "embedded": {
                "dashboard_id": embedded.dashboard_id,
            },
        }
        return self.render_template(
            "superset/spa.html",
            entry="embedded",
            bootstrap_data=json.dumps(
                bootstrap_data, default=json.pessimistic_json_iso_dttm_ser
            ),
        )

In this code, the @talisman(frame_options_allow_from="*") decorator is used to set the X-Frame-Options header to allow framing from any origin for the /embedded endpoint. Adjust the frame_options_allow_from parameter as needed to specify the allowed origins [1].

To continue talking to Dosu, mention @dosu.

Comment From: eschutho

I think what you proposed sounds reasonable. For your specific use-case, do you have a limited set of websites that you want to allow to frame your content, because you can also use the csp to limit attacks if you only allow framing to trusted partner sites.

Comment From: mariomourao

Hi, Where should I apply the changes suggested by the @dosubot?

Is there a way to use an embedded dashboard with TALISMAN_ENABLED set to TRUE?"

Comment From: jesperkonincks

I have the same problem, tried the dosubot fix but that doesn't work.

Comment From: dosubot[bot]

To apply the changes suggested by the @dosubot, you should modify the superset/embedded/view.py file to include the @talisman decorator with the desired configuration for the /embedded endpoint. This allows you to set more relaxed X-Frame-Options specifically for this endpoint.

Additionally, to use an embedded dashboard with TALISMAN_ENABLED set to True, you can configure your superset_config.py file as follows:

TALISMAN_ENABLED = True
TALISMAN_CONFIG = {
    "content_security_policy": {
        "frame-ancestors": ["*.my-domain.com", "*.another-domain.com"],
    }
}
HTTP_HEADERS = {
    'X-Frame-Options': 'ALLOWALL'
}

This configuration allows embedding the dashboard by specifying the domains in frame-ancestors where the dashboard is allowed to be embedded. Make sure to replace the domain names with your actual domains. After making these changes, restart Superset to apply the new configuration [1].

To continue talking to Dosu, mention @dosu.


Help Dosu learn! Give it feedback: Great Response | Irrelevant Answer | Incorrect Sources | Too Verbose | Hallucination | Other

Comment From: patheard

Using the above idea, I made a slight tweak that's working on my end which doesn't wipe out the other Talisman CSP rules:

# In my config.py
from superset.config import TALISMAN_CONFIG

csp_config = TALISMAN_CONFIG.get("content_security_policy", {})
csp_config["frame-ancestors"] = ["'self'", "localhost:3000"] # set to whatever domain is hosting the frame
TALISMAN_CONFIG["content_security_policy"] = csp_config

Comment From: devyash786

@dosu How to implement above changes if i am using heml chart to deploy

Comment From: rusackas

Sounds like there's not a Superset bug here, but rather a configuration issue. I'll close it since it's been silent for months, but if anyone is still facing this, I'm more than happy to reopen it and convert it to a Discussion.