skip to content

What is X-Frame-Options, how does Spring Security set it by default, and how do you change it (e.g. for an H2 console or same-origin iframe)?

level: middleimportance: should knowfreq 55%

answer

  1. DENY default = anti-clickjacking
  2. sameOrigin() for H2 console
  3. ALLOW-FROM is dead → use CSP frame-ancestors
  4. XFrameOptionsHeaderWriter
  5. disable() removes protection

basics

~10 s

X-Frame-Options controls whether your page can be embedded in a frame, preventing clickjacking. Spring Security defaults to DENY. To allow same-origin framing (e.g. the H2 console) use headers().frameOptions(frame -> frame.sameOrigin()).

solid answer

~40 s

X-Frame-Options is an anti-clickjacking header telling the browser whether a page may be rendered inside a `<frame>`/`<iframe>`. Spring Security sends `DENY` by default (no framing at all), written by `XFrameOptionsHeaderWriter`. The two practical modes are `DENY` and `SAMEORIGIN`; the old `ALLOW-FROM` is deprecated and unsupported by modern browsers — use CSP `frame-ancestors` for allow-listing specific origins instead. The classic gotcha: the H2 database console renders itself in frames, so with the default DENY it shows a blank/broken page. Fix by allowing same-origin framing: `http.headers(h -> h.frameOptions(f -> f.sameOrigin()))`. You can disable the header entirely with `frameOptions(FrameOptionsConfig::disable)`, but that removes clickjacking protection and is rarely justified. For fine-grained control, prefer the CSP `frame-ancestors` directive, which supersedes X-Frame-Options in modern browsers.

code

java · 10 lines
java
@Bean
SecurityFilterChain h2Console(HttpSecurity http) throws Exception {
    http
        .securityMatcher(PathRequest.toH2Console())
        .authorizeHttpRequests(a -> a.anyRequest().permitAll())
        .csrf(csrf -> csrf.disable())
        // H2 console renders in frames; default DENY breaks it.
        .headers(h -> h.frameOptions(frame -> frame.sameOrigin()));
    return http.build();
}

go deeper

for a junior

Know it prevents clickjacking and defaults to DENY.

for a middle

Configure sameOrigin() for the H2-console case and know disable() exists.

for a senior

Explain why ALLOW-FROM is dead and CSP frame-ancestors is the modern replacement.

for a principal

Scope framing exceptions to specific chains/paths rather than weakening the global policy.

## Clickjacking, the threat **Clickjacking** is an attack where a malicious site loads your page inside a transparent/obscured `<iframe>` and tricks the user into clicking something (e.g. a 'transfer money' button) they can't see. The defense is to forbid your page from being framed by untrusted origins. ## X-Frame-Options This response header instructs the browser about framing: - **DENY** — never allow the page in any frame. - **SAMEORIGIN** — allow framing only by pages of the same origin (same scheme+host+port). - **ALLOW-FROM uri** — *deprecated*; no modern browser honors it. Do not rely on it. ## Spring Security defaults Spring Security's `XFrameOptionsHeaderWriter` emits **`X-Frame-Options: DENY`** by default. This is the safest baseline. ## Configuring it Inside the headers DSL: ```java http.headers(h -> h.frameOptions(frame -> frame.sameOrigin())); ``` or to turn it off entirely (loses protection): ```java http.headers(h -> h.frameOptions(frame -> frame.disable())); ``` ## The H2 console gotcha Spring Boot's embedded **H2 console** renders its UI using HTML frames. With the default `DENY`, the console appears blank. The canonical fix is to allow same-origin framing for that path (often combined with a dedicated filter chain or a request matcher). This is the single most common reason developers touch `frameOptions`. ## Modern replacement: CSP frame-ancestors X-Frame-Options is a coarse, legacy mechanism. The **Content-Security-Policy `frame-ancestors`** directive is its modern superset: it can allow-list specific origins (`frame-ancestors 'self' https://trusted.example.com`) and, when present, browsers prefer it over X-Frame-Options. For anything beyond DENY/SAMEORIGIN, use CSP `frame-ancestors`, not the dead ALLOW-FROM. ## Gotchas - Setting both X-Frame-Options and a conflicting CSP `frame-ancestors` can confuse — keep them consistent; browsers give precedence to CSP. - `sameOrigin()` compares full origin (scheme+host+port); a mixed http/https or subdomain will be treated as cross-origin. - Disabling the header globally to fix one embedded page is over-broad; scope it to that path.

  • Why can't you just use X-Frame-Options: ALLOW-FROM to permit a specific partner site to frame your page?
    ALLOW-FROM is deprecated and unsupported by modern browsers (Chrome never implemented it). The correct approach is the CSP directive `frame-ancestors 'self' https://partner.example.com`, which browsers honor and which takes precedence over X-Frame-Options.

saying these in an interview costs you the question

  • Recommending ALLOW-FROM to allow-list an origin
  • Disabling frameOptions globally just to make the H2 console work
  • Confusing SAMEORIGIN (same scheme+host+port) with same domain/subdomain
  • Not knowing DENY is the default

context