Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Security › Web attacks and defenses

Clickjacking and open redirects

How do you stop your page from being hidden inside another site's page, or your URL from sending users to another site?

Clickjacking is when another site puts a page of your site in an invisible frame, so that the user clicks a button on your page without knowing it. An open redirect is when your server takes the "URL to return to after login" from the request and redirects to it without checking, so a link that starts with your site's address ends up on another site. In both cases the server is not broken into. Instead, people's trust in your site is exploited, and clickjacking is blocked by the browser according to the response header the server sends, and an open redirect is blocked by the server checking the URL.

Checked against real execution · Response headers were checked against the actual responses of a Spring Boot 4.1.1 (Spring Security 7.1.1) server for 24 combinations, and browser behavior was checked with Chromium 151. Other browsers follow the documentation

Concepts in this lab

Clickjacking
An attack in which another site puts a page of your site in a frame (iframe) and makes it invisible, so that the user believes they are clicking something else but actually clicks a button on your page. The click is made by the user's own browser, so the request the server receives cannot be told apart from a normal click. So instead of picking out requests, the server blocks the attack by telling the browser in a response header not to put the page in a frame. It is the browser that actually enforces this, so a page without the header can be put in a frame by anyone.
X-Frame-Options and frame-ancestors
Two ways to tell the browser that receives a response who may put the page in a frame. One is the X-Frame-Options header, and the other is the frame-ancestors directive written inside the Content-Security-Policy header. X-Frame-Options takes one of two values, DENY (no page may frame it) and SAMEORIGIN (only pages with the same origin may), while frame-ancestors lists the origins that may frame the page, so it can allow specific other sites. Spring Security adds X-Frame-Options: DENY even if you configure nothing. When both come together, current browsers follow frame-ancestors and ignore X-Frame-Options, so the two should be set so that they do not disagree.
Open redirect
A vulnerability in which the server redirects to an address taken from the request without checking it. It arises in features added for convenience, such as a "URL to return to after login", and it makes a link that starts with your site's address a passage to another site. Users trust the first part of the address, so this is used to lure them to a fake login page. To prevent it, accept only values that are acceptable, such as paths on the same site, and also check that an address that starts with // points to a different host. A more reliable way is to accept a predefined name instead of an address and have the server look up the address in its own list.
Spring Security and SecurityFilterChain
Spring Security is Spring's security framework for handling authentication and authorization, and it works as a set of Servlet filters. A SecurityFilterChain defines which security filters apply to which requests and in what order. Before a request reaches the controller, the filters run one after another and do work such as checking the authentication information and checking permissions.
CORS and the same-origin policy
The same-origin policy is a browser security policy that stops a script from one origin (the combination of scheme, host, and port) from reading responses from another origin. CORS is a way to relax this restriction: the server uses response headers to say that it allows access from specific origins. The browser applies this policy, so it does not apply to calls between servers or to calls made with tools.

Try these

Running with the default settings

  1. The user opens the page at https://attacker.example. Only a "Claim prize" button is visible. But this page overlays the shop.example payment page on that button as a frame (iframe) and makes it transparent so that it cannot be seen.
  2. The browser requests https://shop.example/pay to get the page to put in the frame. The page inside a frame is fetched from the server in the same way as when you type an address in the address bar.
  3. shop.example responds with the payment page. Because frameOptions is turned off in the settings, the response has no frame-related headers at all.
  4. The browser checks the response headers: nothing says the page must not be put in a frame. A page like this can be framed by any site, so the browser renders the payment page inside the frame.
  5. The user clicks "Claim prize". What is visible is a button on the attacker's page, but a transparent payment page is overlaid on it. What was actually clicked is the [Pay] button of shop.example. If the user was logged in to shop.example, they have approved a payment without knowing it.

Result of this run

Code

// Spring Security configuration of the shop.example server
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // (settings about who may access what are omitted)
    http.headers(h -> h.frameOptions(f -> f.disable()));
    return http.build();
}

// Frame-related headers on the /pay response with this configuration
//   (none)

<!-- Framing side: a page on https://attacker.example -->
<iframe src="https://shop.example/pay"></iframe>

Assumptions of this model

The 5 frame-related header cases and the 9 Location header cases were checked against the actual responses of a Spring Boot 4.1.1 (Spring Security 7.1.1, embedded Tomcat 11) server started on 127.0.0.1. Browser behavior (15 cases of whether the page is rendered inside the frame, and 9 cases of which server the browser reaches by following Location) was checked by running Chromium 151 in headless mode, with origins imitated by different ports on 127.0.0.1. Other browsers were not run, and the documentation was followed: RFC 7034 and the HTML Standard for X-Frame-Options, the W3C Content Security Policy Level 3 for frame-ancestors, RFC 3986 section 4.2 for addresses that start with //, and the WHATWG URL Standard for treating a backslash like a slash. Simplifications: only one level of framing is covered (with several levels, the browser checks all the outer pages). How a page is overlaid transparently is not included. Cookies with SameSite=Lax are not attached to requests sent from inside a frame on another site (RFC 6265bis draft), and Chromium-based browsers treat cookies with no SameSite attribute as Lax too. So clickjacking of a page that requires login mainly works when the session cookie is SameSite=None. This cookie part was not run. The ALLOW-FROM value of X-Frame-Options is not covered because current browsers do not support it. Location is converted to an absolute URL with the Spring Boot default (server.tomcat.use-relative-redirects=false), and a server with this value set to true sends the received value as it is. With that setting, a value of the form /\attacker.example also passed the check that only looks for "/", and Chromium was confirmed to go to another server. The Location shown on screen is the address of the test server rewritten as https://shop.example.

Previous lab: CORS · Next lab: Information leakage

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these