Securing an Embedded Website with CSP and X-Frame-Options

We host a small (.Net) website for our customers; it’s designed to be embedded within both our own hosted pages, and potentially within the customers own web applications (both internal and external). By far the most common way of doing this is embed it within a iframe on the site.

So our site is explicitly designed to be within a frame; however, every vulnerability and web application security scan we run complains bitterly that we haven’t set the Content-Security-Policy and X-Frame headers correctly. We currently have the CSP set as

Content-Security-Policy: default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval' 'self'; style-src 'unsafe-inline' 'self'; img-src 'self' data:

(Why all those unsafe? Because we use a component suite (DevExpress) that explicitly requires it.)

Even ignoring that, our security scans (via Tenable) complain that:

- 'frame-ancestors' should be set to 'none' to avoid rendering of page in <frame>, <iframe>, <object>, <embed>, or <applet>.

We’re also "missing" an X-Frame-Options header:

The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page inside a frame or iframe. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites.

.. but the only options for this are DENY or SAMEORIGIN – we don’t want either.

What CSP and X-Frame-Options should we specify for a site that is explitly designed to be embedded within another page?

EDIT: To give more context about the site in question: it’s a hosted service, designed to be used by our customers for their own internal staff – it’s not used, presented or designed to be available to public users (although the site is publically accessible, more for convenience for our customers to be able to access it from whereever their staff are).

More pertenantly: although the site is used to facilitate payments, it doesn’t present or collect sensitive details in any way. In effect, it’s just a "status" page – it shows the user the status of a transaction that’s taking place in real time – it’s effectively "read only" – and the only way for that transaction to progress is via a separate, and completely out-of-band process to be taking place. Without this process progressing, the page effectively just shows "Waiting …".

Solution: Dynamically Generate X-Frame-Options and frame-ancestors

The usual solution here is to dynamically generate the XFO / frame-ancestors directives. Require, in the URL (probably as a query parameter), a token that identifies the page where it’s to be embedded. If no such token is presented, or if the token doesn’t match a known site, don’t allow framing at all (or make it same-origin only, at least). This doesn’t add a lot of security (none at all if you allow arbitrary domains; only a little if you have a specifically allowed list of allowed customers) but it’ll make the scanners happy (well, happier; that CSP is a travesty barely better than having none at all).


Do bear in mind that a web app – that is, any page with dynamic content and user interaction – that is embedded is inherently quite risky. The extremely vast majority of users aren’t going to know whether they’re interacting with you or an imposter. Checking the URL bar doesn’t help; they’d have to use the dev tools to check the source of the iframe, which isn’t exposed to the normal browser UI anywhere at all. To approximately quote my own comment:

Do your users enter into your page (the one being embedded, NOT your customer’s pages) any sensitive information – credentials including OTPs, PII, payment info, non-public communications, PHI, etc. – that they specifically trust you (rather than your customers) with? Because if so, your entire security model is broken; an attacker can impersonate your embedded page and steal that info from your users who have no realistic way to know whether they’re interacting with you or a phishing page.

For a simple example, suppose you have an embeddable webapp, call it Talkr, that allows users to make comments and vote on them on arbitrary pages. Users have accounts that let them have a consistent shared identity across multiple sites, and build a trusted reputation (or filter out bad commenters). To log into their Talkr account on a given page, the user enters their email address into a login form on the embedded Talkr iframe, you send them a one-time password (OTP), and they type it into the iframe.

Suppose I, a malicious site owner, want to impersonate your users. I "embed" a fake Talkr login page into my site. It looks identical to the real one, and the user has no realistic way to know the difference. When a user enters their email address into my phishing page, I log the address and also send – from my server – that email address to your real Talkr login service. You send the user an email with an OTP, and my login phishing page changes to expect the OTP, just like the real one does. The user checks their email and enters the OTP into my phishing page. I collect the OTP and forward it to your server, again pretending to be the user. You send me back the session token that the user should have gotten. I use it to pull and display the user’s data (just like the real Talkr site does); the user is none the wiser.

I can do this to as many users as visit my page and try to comment! I can now post arbitrary comments under their names, and probably also do other things like see what other sites they’ve commented on and vote on other comments… even on sites that aren’t my own! It’s basically a classic phishing attack for account takeover, with the only twists being the use of OTPs (which I added because some people think that makes them safe against phishing; it does not) and the fact that the user can’t tell that the login page is fake even if they check the URL carefully because the login page is always on a third-party site.

Leave a Reply

Your email address will not be published. Required fields are marked *