Enter a URL to see whether it can be embedded in an iframe
Paste a URL to find out whether it will load inside an iframe on another site. The tool fetches the page from our edge, follows redirects the way a frame does, and reads the two headers that control framing: X-Frame-Options and the frame-ancestors directive of Content-Security-Policy. You get a verdict for any other site, for the page's own origin and for your domain if you enter it, plus a live preview and ready-to-paste embed code. Your browser cannot run this check by itself, because it never lets one site's JavaScript read another site's response headers.
Enter a URL to see whether it can be embedded in an iframe
The checks follow the HTML Standard and CSP Level 3, which current Chrome, Firefox and Safari implement, and run in the order a browser runs them. Mixed content comes first, because an https page cannot frame an http:// page at all. Next, if the response has a Content-Security-Policy with frame-ancestors, that list alone decides and X-Frame-Options is ignored, even when it is stricter. Without frame-ancestors, X-Frame-Options: DENY blocks every parent and SAMEORIGIN allows only pages on the exact same scheme, host and port. With neither header, any site can frame the page.
If a page sends both headers, only its frame-ancestors row applies.
| Header on the framed page | Who can embed it |
|---|---|
| No X-Frame-Options and no frame-ancestors | Any site |
| X-Frame-Options: DENY | Nobody |
| X-Frame-Options: SAMEORIGIN | Only pages on the same scheme, host and port |
| X-Frame-Options: ALLOW-FROM https://a.example | Any site, because browsers ignore the value |
| X-Frame-Options: DENY, SAMEORIGIN | Nobody, because conflicting values count as DENY |
| frame-ancestors 'none' | Nobody |
| frame-ancestors 'self' | Only its own origin |
| frame-ancestors 'self' https://partner.example | Its own origin and https://partner.example |
| frame-ancestors https://*.example.com | Subdomains of example.com, but not example.com itself |
| frame-ancestors * | Any http or https site |
| frame-ancestors in Content-Security-Policy-Report-Only | Any site, because report-only never blocks |
| frame-ancestors in a <meta> tag | Ignored, so the HTTP headers decide |
Sources in frame-ancestors follow the CSP Level 3 matching rules:
https://*.example.com covers subdomains but not
example.com itself, a source without a port only matches the
default port, an http source also matches https, and a source with
a path never matches, because ancestors are compared as bare
origins. Both headers apply to every ancestor, so in nested frames
each level has to be allowed.
Send one header from the server, not a meta tag, because browsers
ignore frame-ancestors and X-Frame-Options in HTML. List every
origin that may embed the page; keep 'self' if your own
pages frame it too.
# Only your own pages and one partner may frame it Content-Security-Policy: frame-ancestors 'self' https://partner.example # Nobody may frame it (clickjacking protection) Content-Security-Policy: frame-ancestors 'none' # nginx add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example" always; # Apache (.htaccess, needs mod_headers) Header always set Content-Security-Policy "frame-ancestors 'self' https://partner.example"
If the page already sends a Content-Security-Policy, add frame-ancestors to that header. A second policy is enforced on top of the first, so any directive that appears in both has to pass twice. Once frame-ancestors is in place, X-Frame-Options only matters to browsers too old to know CSP Level 2.
# -L follows redirects and prints the headers of every hop; the last block is the one that counts
curl -sIL https://stripe.com | grep -iE "^(HTTP/|x-frame-options:|content-security-policy:)"
# Only the frame-ancestors directive
curl -sIL https://stripe.com | grep -i "^content-security-policy:" | tr ";" "\n" | grep -i frame-ancestors -I sends HEAD, and a few servers answer it with different headers. curl -sL -o /dev/null -D - <url> prints the headers of a real GET instead.
// Simplified: handles 'none', 'self', * and exact or *.wildcard hosts.
// The tool also applies the CSP3 rules for schemes, ports, paths and multiple policies.
async function canFrame(url, parentOrigin) {
const res = await fetch(url); // follows redirects
const self = new URL(res.url).origin;
const csp = res.headers.get("content-security-policy") ?? "";
const fa = csp.split(";").map((d) => d.trim().split(/\s+/))
.find(([name]) => name.toLowerCase() === "frame-ancestors");
if (fa) {
const parentHost = new URL(parentOrigin).hostname;
return fa.slice(1).some((src) => {
src = src.toLowerCase();
if (src === "*") return true;
if (src === "'self'") return parentOrigin === self;
const host = src.replace(/^[a-z]+:\/\//, "").replace(/[:/].*$/, "");
return host.startsWith("*.") ? parentHost.endsWith(host.slice(1)) : parentHost === host;
});
}
const xfo = (res.headers.get("x-frame-options") ?? "").trim().toLowerCase();
if (xfo === "deny") return false;
if (xfo === "sameorigin") return parentOrigin === self;
return true; // no header, or a value browsers ignore
}
console.log(await canFrame("https://stripe.com", "https://app.contentful.com")); // true
console.log(await canFrame("https://github.com", "https://example.com")); // false Run it on a server. In a browser, fetch() to another origin either fails on CORS or hides these headers, which is why the tool runs on the edge.
import requests
def frame_headers(url):
r = requests.get(url, timeout=10) # follows redirects
csp = r.headers.get("Content-Security-Policy", "")
fa = next((d.strip() for d in csp.split(";") if d.strip().lower().startswith("frame-ancestors")), None)
return {
"final_url": r.url,
"x_frame_options": r.headers.get("X-Frame-Options"),
"frame_ancestors": fa, # when set, browsers ignore X-Frame-Options
}
print(frame_headers("https://github.com"))
# {'final_url': 'https://github.com/', 'x_frame_options': 'deny', 'frame_ancestors': "frame-ancestors 'none'"} Both headers are checked against every ancestor up to the top-level page. If site A frames site B, and B frames the page, A and B must both be allowed. The verdicts here assume one level, which is how almost every embed works.
The check runs from Cloudflare's network with no cookies. A page that sends different headers to logged-in users, or a bot challenge to unknown clients, can look different to a real visitor; the live preview shows what your own browser does with the real response.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
The site sends X-Frame-Options: DENY or SAMEORIGIN, or a Content-Security-Policy frame-ancestors list that does not include your domain. The browser fetches the page, reads the header and shows an error such as "refused to connect" instead of the content. No attribute on your iframe can override it; only the site's owner can change the header.
Both tell the browser which pages may frame a URL. X-Frame-Options is the older header and has two working values, DENY and SAMEORIGIN. CSP frame-ancestors replaces it and takes a list: 'none', 'self', exact origins, schemes and wildcards such as https://*.example.com. When a response has both, browsers follow frame-ancestors and ignore X-Frame-Options.
Yes, for the header side. Clickjacking only works if the attacker can load your page in a frame. If a page with logged-in actions, such as account settings or a delete button, comes back as embeddable by any site, it has no protection. Fix it with Content-Security-Policy: frame-ancestors 'none', or 'self' if your own pages frame it. The check runs without cookies, so if your server sends different headers to logged-in users, also test with curl and your session cookie.
No. Chrome and Safari never supported it, and Firefox removed it. A browser that does not recognize the value ignores the whole header, so a page that sends ALLOW-FROM can be framed by any site. Use Content-Security-Policy: frame-ancestors https://partner.example instead.
Either the src is http:// on an https page, which the browser blocks as mixed content, or the page runs frame-busting JavaScript that hides its content or tries to replace the parent page when it detects a frame. Content behind a login is the other common case: cookies without SameSite=None; Secure never reach a cross-site frame, Safari blocks third-party cookies and Firefox partitions them.
The preview is framed by webdevutility.com, not by your site, so it shows what any other website gets. A page that lists specific domains in frame-ancestors blocks the preview but still works on those domains. The Your site row is the exact answer for your origin.
Look for its official embed URL. YouTube watch pages send X-Frame-Options: SAMEORIGIN, but the /embed/ URLs allow framing, and Google Maps and most social networks have similar embed links. Otherwise link to the page or ask the owner to add your origin to frame-ancestors. Proxying the page through your own server to strip the headers breaks logins and usually breaks the site's terms too.
Follow every hop of the src with the redirect checker, confirm the page is up with the website down checker, or look up what a 403 or 404 in the result means in the HTTP status code list. For a widget made of an element and a script tag, which runs in your page rather than in a frame, use the embed code tester.