Week 10

Security Mindset

Threat Modeling Basics

Common Backend Vulnerabilities

Handling Sensitive Data

Hashing vs. Encryption

How to Store Passwords

Authentication & Authorization

Spring Security

Spring Security JWT authentication

Practice

Assignment

Backend Track

Introduction

Our methodology for this section is "Exploit First, Then Fix." For each vulnerability, we will first put on our "attacker hat" to understand how it can be exploited. And will focus on the concepts and the attacker's strategy, then look at how that translates into code.

1. Broken Access Control (IDOR)

Every request asks two questions: who are you? (authentication) and are you allowed to do this? (authorization). Broken Access Control is when the second question never gets asked. The most common form is IDOR (Insecure Direct Object Reference): the application trusts an ID coming from the user and returns whatever matches, without checking whether that user is allowed to see it.

The Attacker's Goal:

A Practical Example:

You are logged in and view your own order at GET /api/orders/123. Out of curiosity, you change the number to GET /api/orders/124. If order 124 belongs to another customer and the server returns it anyway, that's IDOR — and an attacker can loop through every ID to download the whole database.

How to avoid it as a backend engineer:

Vulnerable — returns any order to anyone who asks:

Order order = orderRepository.findById(orderId)
        .orElseThrow(() -> new NotFoundException("Order not found"));
return order;

Fixed — confirms the order belongs to the current user first:

Order order = orderRepository.findById(orderId)
        .orElseThrow(() -> new NotFoundException("Order not found"));

if (!order.getOwnerId().equals(currentUserId)) {
    throw new AccessDeniedException("This order does not belong to you");
}
return order;

You'll see how Spring Security helps enforce these checks in Spring Security

<aside> ❗

A note on ID types: From now on, avoid PostgreSQL's auto-incrementing serial for IDs that appear in URLs. Sequential numbers (1, 2, 3…) are trivial to guess and loop through, which is exactly what makes IDOR easy to exploit at scale. Prefer a random, non-guessable ID like a UUID.

</aside>

2. SQL Injection

Imagine you ask a librarian for a book by "John Smith." Now imagine you ask for "John Smith's book — and also hand me the keys to the rare books room." SQL Injection is that second request. It happens when an attacker hides database commands inside what looks like normal input (a username, a search box), and the application runs them without realising.

The root cause is always the same: the application builds a query by gluing user input directly into the SQL string, so the database can't tell where your command ends and the user's data begins.

The Attacker's Goal: bypass the application's logic and talk directly to the database to:

A Practical Example

A login form sends the username and password to the backend, which builds a query by concatenating the input directly into the SQL string:

String sql = "SELECT * FROM users WHERE username = '" + username + "'";

For a normal username like testuser, that produces a harmless query. But an attacker types this into the username field instead:

' OR '1'='1' --

The query now becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' --'

'1'='1' is always true, so the WHERE matches every row, and -- turns the rest of the line into a comment, throwing away the password check. The database happily returns a user, and the attacker is logged in.

How to avoid it as a backend engineer:

The fix is to stop mixing code (the query) with data (the user's input). Keep them separate, and the database will always treat input as a plain value — never as part of the command.

Vulnerable — user input becomes part of the SQL command:

String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// username = ' OR '1'='1' --  → the WHERE is always true, password check is skipped

Fixed — user input is sent as a value, never executed as code:

User user = jdbcClient.sql("""
                SELECT * FROM users
                WHERE username = :username
                """)
        .param("username", username)
        .query(USER_ROW_MAPPER)
        .single();

<aside> 💭

The fix isn't a clever library — it's "never let user input become part of the query." Prepared statements make that separation automatic.

</aside>

3. Cross-Site Scripting (XSS)

XSS happens when an application takes untrusted data (like an attackers crafted javascript code in a comment section) and sends it to a web browser without cleaning it first, causing a malicious script to run in the victim's browser. Example

<p>Hi, my name is Bob.</p><script>steal_user_cookie();</script>

The Attacker's Goal:

How to avoid It as a backend engineer:

4. Cross-Site Request Forgery (CSRF)

CSRF tricks a logged-in user into unknowingly performing an action they didn't intend — for example, a malicious page quietly makes their browser send a request to a site where they're already logged in, to change their email or transfer money.

Whether an application is exposed to CSRF depends on how it keeps users logged in, which is something you'll cover in Authentication & Authorization. For now, just know the attack exists and that the defence is tied to your authentication method — we'll come back to it there.

5. Improper Error Handling: Information Disclosure

This is when an application reveals sensitive internal details (like stack traces or database error messages) to the end-user when it encounters an error.

The Attacker's Goal: Gather intelligence for a future attack by learning about the technologies used (database, framework), internal file paths, and application logic.

How to avoid It as a backend engineer:

6. Insufficient Rate Limiting

This vulnerability exists when an application doesn't limit how many times a user or IP address can attempt an action in a given period of time.

The Attacker's Goal:

How to avoid It as a backend engineer:

Extra resources

Reading

Videos