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
- Default settings — A server with the frame header turned off. An attacker can put my payment page in a hidden frame.
- Spring Security default — Even with no configuration, X-Frame-Options: DENY is added, so the page is not rendered in a frame.
- Frame on my own page under the default — DENY blocks frames from the same site too.
- Allow only the partner site — The page is rendered only in frames from the origins listed in frame-ancestors.
- If the return URL is not checked — A link that starts with my site's address ends up on the attacker's site.
- If only "/" is checked — An address that starts with // passes the check.
- Allow only same-site paths — A value that points to another site is replaced with the home page.
Running with the default settings
- 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.
- 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.
- shop.example responds with the payment page. Because frameOptions is turned off in the settings, the response has no frame-related headers at all.
- 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.
- 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
- Without a header that says not to frame the page, the browser renders it in a frame on any site. If you turn off frameOptions altogether to show one screen that uses frames (for example, the H2 console), every page ends up in this state. Changing it to sameOrigin instead of turning it off is safer.
- From the requests the server receives, clickjacking cannot be told apart from a normal click. So the server blocks it by announcing in advance, with a response header, that "this page must not be put in a frame", and the browser obeys.
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