Trouble with X-Frame-Options through iframe

1. Caddy version (caddy version): v2.0.0

2. How I run Caddy:

As a service within debian server

a. System environment:

Debian GNU/Linux 9.13 (stretch)

b. Command:

sudo service caddy start

d. My complete Caddyfile or JSON config:

In caddy_security.conf :

header {
    # keep referrer data off of HTTP connections
    Referrer-Policy no-referrer-when-downgrade
    # Referrer-Policy "strict-origin-when-cross-origin"

    # enable HSTS
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

    # Enable cross-site filter (XSS) and tell browser to block detected attacks
    X-Xss-Protection "1; mode=block"

    # disable clients from sniffing the media type
    X-Content-Type-Options "nosniff"

    # clickjacking protection
    X-Frame-Options "DENY"

    Content-Security-Policy "upgrade-insecure-requests"
}

My Caddyfile config :

qqq.domain.fr {
	import ./caddy_security.conf

	encode zstd gzip

	reverse_proxy http://localhost:8738 {
	}
}

domain.fr {
	import ./caddy_security.conf

	encode zstd gzip

	route /calepin/* {
		uri strip_prefix /calepin

		reverse_proxy http://localhost:9001 {
			header_down X-Frame-Options "SAMEORIGIN"
		}
	}

	# Static file server
	root * /home/xxx/
	file_server

	#uri strip_suffix .php
	try_files {path} {path}/ {path}.html
}

3. The problem I’m having:

Within qqq.domain.fr, I want to be able to display an iframe of domain.fr/calepin/etc

I thought setting the header_down on the reverse_proxy would do the trick but even though I DO get the SAMEORIGIN in the response header when I access domain.fr/calepin/etc directly, I get DENYed when accessing it through the iframe.

4. Error messages and/or full log output:

From the iframe GET response : x-frame-options: DENY
If I accessed directly the response has : x-frame-options: SAMEORIGIN

5. What I already tried:

Adding X-Frame-Options "SAMEORIGIN" as default for domain.fr, adding it as header_up for the proxy, …

The service behind the proxy is using docker and I don’t know nor want to change its config if the problem comes from that. Why wouldn’t the header_down rewrite this option in all cases??

FYI, you can use handle_path instead of route here, and it’ll save you from needing uri strip_prefix, because handle_path does the stripping implicitly.

I think the issue you’re having though is that try_files executes before your route. This is because of Caddy’s default directive order:

I’d instead write your config like this, which may fix the problem:

	handle_path /calepin/* {
		header X-Frame-Options "SAMEORIGIN"
		reverse_proxy http://localhost:9001
	}

	handle {
		root * /home/xxx/
		try_files {path} {path}/ {path}.html
		file_server
	}

Using handle and handle_path ensures that these two routes are handled mutually exclusively, so try_files won’t happen when you’re trying to proxy, and anything not /calepin/* will be handled by your file server.

I also changed your header_down to header which I think is more correct here.

2 Likes

Hello Francis!

I’ll try handle_path if it allows for clearer config, but my route works just fine when accessed directly so the problem doesn’t come from that.

I was trying other stuff in the meantime and I thought maybe I needed to edit the header when proxying in qqq.domain.fr, and indeed if I do this :slight_smile:

qqq.domain.fr {
    import ./caddy_security.conf

    encode zstd gzip
    
    reverse_proxy http://localhost:8738 {
        header_down X-Frame-Options "SAMEORIGIN"
    }
}

the x-frame-options is set well even in the iframe case!!

I would like to say this is enough but I still get the error page “Firefox Can’t Open This Page”

The current response I get :

HTTP/2 200 OK
cache-control: public, max-age=3600, must-revalidate
content-encoding: gzip
content-security-policy: upgrade-insecure-requests
content-type: text/html; charset=utf-8
date: Tue, 02 Feb 2021 05:38:04 GMT
etag: W/"8f19-UxOffE+hz6Mp2raSAD8xWIMg4Qo"
feature-policy: autoplay 'self'
referrer-policy: no-referrer-when-downgrade
strict-transport-security: max-age=31536000; includeSubDomains; preload
vary: Accept-Encoding
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-powered-by: Express
x-ua-compatible: IE=Edge,chrome=1
x-xss-protection: 1; mode=block
X-Firefox-Spdy: h2

The FF error points me to Website will not allow Firefox to display the page if another site has embedded it | Firefox Help and now that x-frame-options is set just fine, I’m looking up the Content-Security-Policy…

EDIT: thanks to chromium and its clear error messages it seems that as the subdomains are different the x-frame-options SAMEORIGINS is not enough…

1 Like

After more time trying things, clearing the browser cache and testing, I have something which works but isn’t perfect:

qqq.domain.fr {
	import ./caddy_security.conf

	encode zstd gzip

	reverse_proxy http://localhost:8738 {
	}
}

domain.fr {
	import ./caddy_security.conf

	encode zstd gzip

	route /calepin/* {
		uri strip_prefix /calepin

		reverse_proxy http://localhost:9001 {
			header_down X-Frame-Options "BULLSHIT" # or simply ""
		}
	}

	# Static file server
	root * /home/xxx/
	file_server

	#uri strip_suffix .php
	try_files {path} {path}/ {path}.html
}

In short, I shouldn’t put anything special in my qqq.domain.fr block, and just needs to set the xframeoptions as I did in the /calepin route, except with a bullshit name that will make it ignored by the browser.

I’ll try to find out how to remove this header instead in the header_down, as setting even an empty string makes the browser output some error log. Also I would prefer to limit the inclusion of this in an iframe only when coming from qqq.domain.fr . If you have any idea?

1 Like

You can remove headers by putting - before the header name, like this:

header_down -X-Frame-Options

Just for kicks, can you try using the header directive instead? I think it should work just the same (with one less line) but I just want to confirm. Just put this before your reverse_proxy line:

header -X-Frame-Options

Also FWIW I still recommend wrapping your config with handle_path and handle, because it’s quite inefficient to have Caddy run try_files on requests where you know it never makes sense (i.e. /calepin/*), since it will do multiple filesystem calls.

2 Likes

I tried using the header directive indeed but it can get overwritten by the proxied service I think. I guess it would work in all cases if I put it after the reverse_proxy block but I didn’t try that.

I didn’t have time earlier to follow your advice about handle_path but I’ll be doing that now! Thanks for the tip!

Do you have an idea how to allow the iframe to work only from my subdomain? What I have in mind is to add a content-security-policy with frame-src at the same place I remove the x-frame-options.

1 Like

Right - I guess you would need to use defer, like this, so that header happens after the proxy:

header -X-Frame-Options {
	defer
}

So I guess you lose out on the terseness benefit there :wink:

Frankly, I don’t. I haven’t done the deep dive on security headers and all their edgecases yet. I don’t typically build things that require deviating from defaults.

1/ This defer option helped me do something else which is nice!

2/ On my instance the handle_path isn’t recognised as a directive, it might be a different version but I don’t see the minimum required version in the docs for that directive.

3/ Also something really weird after more testing: if I let the X-Frame-Options directive in the caddy_security header, then any subsequent header definition of that same header is ignored, whether in a header or header_down. Why would that be?

in caddy_security:

    # clickjacking protection
    X-Frame-Options "SEC DENY"
}

In Caddyfile:

domain.fr {
    header {
        X-Frame-Options "EARLY DENY"
    }

    import ./caddy_security.conf

    header -X-Frame-Options
    
    header {
        X-Frame-Options "LATE DENY"
    }

    handle /calepin/* {
        # handle_path => no need to strip the /calepin/ prefix but it fails now
        uri strip_prefix /calepin

        header X-Frame-Options "CALEPIN DENY"

        reverse_proxy  http://localhost:9001 {
            header_down X-Frame-Options "CALEPIN DOWN DENY"
        }
    }
}

I added all those to check which one would “win”, and when I request it’s always : “SEC DENY” (so either direct GET or via an iframe)…?

1 Like

Oh, yeah you’re using a very old version. Please upgrade to v2.3.0

I think you misunderstand how the Caddyfile works - as I linked to earlier, directives are sorted according to a predetermined list, so the position inside your Caddyfile does not affect behaviour. The exception is route which allows you to override the order, but route blocks themselves will be ordered at the top level according to the list.

This means your “early” and “late” headers will happen at the same time.

Also import is basically just copy-paste so directives in there will also be subject to the Caddyfile directive sorting.

2 Likes

Oh indeed I will fix my installation! I think I installed it manually at the time and the v2 was new and shiny.

I thought the order of entry would impact the order of precedence… So how am I supposed to have a default header and set another one on a case by case basis?

In this case as they are all the same header directives, how is it sorted? Thanks for the info!

With request matchers.

I think in this case, they’ll be grouped up together but in the order they appear in the Caddyfile. You can find out but running caddy adapt --pretty on your config to see the underlying JSON for you config.

This topic was automatically closed after 30 days. New replies are no longer allowed.