fix(preview): honour frame-ancestors when detecting embedding restrictions#151
Merged
Conversation
…tions The build-time check only flagged a target as unembeddable when it sent X-Frame-Options or a CSP containing `frame-ancestors 'none'`. It never checked whether the frame-ancestors list actually permits the embedding origin, so a target sending the far more common `frame-ancestors 'self'` was treated as embeddable. The browser then refused the iframe at runtime and the visitor saw an empty box, because the fallback that exists for exactly this case was never rendered. The check was also inert in practice. It issues a HEAD request, but Hugo only whitelists GET and POST by default (security.http.method), so resources.GetRemote failed on every Hinode site that had not opted in, and the error path silently assumed the target was embeddable. - Evaluate frame-ancestors against this site's own origin, derived from site.BaseURL. Implements the host-source grammar: '*', 'self', scheme-sources, an optional scheme, wildcard subdomains (which do not match the apex domain), and ports (defaulting to the scheme's default port). - Apply every policy in the response, so the most restrictive one wins. - Ignore X-Frame-Options when the CSP declares frame-ancestors, per spec, and honour SAMEORIGIN rather than treating any value as a block. - Retry with GET when HEAD is refused, so the check works on the default security config. - Warn, and still render the iframe, when the headers cannot be fetched at all. Verified against a local server returning synthetic headers, covering frame-ancestors 'none', 'self', explicit hosts, wildcard hosts, scheme-sources, port mismatches, X-Frame-Options DENY/SAMEORIGIN, CSP-over-XFO precedence and absent headers, plus four live sites. All 15 cases produce the correct verdict over both the HEAD and the GET path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
🎉 This PR is included in version 2.0.3 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The preview component renders a live iframe and, when it detects the target refuses to be framed,
falls back to a screenshot. Two defects meant the fallback almost never fired, so visitors got an
empty box where the preview should be.
1.
frame-ancestorswas only checked for'none'.A target sending the far more common
frame-ancestors 'self'was treated as embeddable. Thebrowser then refused the iframe at runtime — and nothing renders the fallback, because the client
script has no load-failure handling. This is what broke the live previews on
gethinode.com/showcases.
2. The check never ran at all on a default config.
It issues a
HEADrequest, but Hugo only whitelistsGETandPOSTby default(
security.http.method) — the effective policy on a stock Hinode site is:so
resources.GetRemotefailed every time, and the error path silently assumed the target wasembeddable. Only gethinode.com, which explicitly whitelists
HEAD, ever exercised the check.Change
frame-ancestorsagainst this site's own origin, derived fromsite.BaseURL,instead of only looking for
'none'. Implements the host-source grammar:*,'self',scheme-sources (
https:), an optional scheme, wildcard subdomains (which correctly do notmatch the apex domain), and ports (defaulting to the scheme's default port).
X-Frame-Optionswhen the CSP declaresframe-ancestors, per spec, and honourSAMEORIGINproperly rather than treating any value as a block.GETwhenHEADis refused, so the check works on the default security config.failing silently.
The logic lives in three inline partials (
preview-origin,preview-source-match,preview-embeddable), following the file's existingpreview-btn-groupconvention.Verification
Built the exampleSite as if it were
https://gethinode.com, against a local server returningsynthetic headers, plus four live sites. Before: 8 of 15 wrong — every case rendered a live
iframe, including
frame-ancestors 'none'andX-Frame-Options: DENY, confirming the check wascompletely inert. After: 15/15 correct, over both the
HEADand theGETpath.frame-ancestors 'self' gethinode.comframe-ancestors 'self' *.gethinode.comframe-ancestors 'self'frame-ancestors 'none'frame-ancestors *frame-ancestors https:frame-ancestors gethinode.com:8443X-Frame-Options: DENYX-Frame-Options: SAMEORIGINXFO: DENY+frame-ancestors gethinode.compnpm test(exampleSite build) passes.Related
The showcase previews also need CSP config fixes, which are separate:
gethinode/gethinode.com#154, gethinode/hinode#2008, gethinode/theme-agency#384. This PR is what
guarantees a graceful screenshot rather than a blank box whenever a target does refuse framing.
🤖 Generated with Claude Code