How Hackers Attack Web Applications — XSS, CSRF, SQL Injection Explained

I walk through XSS, CSRF, and SQL injection from the attacker's side — the mechanics, the targets, and the code patterns that signal an exploitable application.

How Hackers Attack Web Applications — XSS, CSRF, SQL Injection Explained

Every developer who has implemented CSRF tokens or output-encoded HTML has read the advice from the defender’s side. What’s less common is understanding the view from the other chair.

XSS, CSRF, and SQL injection don’t work the same way. Each one exploits a different trust relationship built into the web: the browser trusts scripts that look like they came from your domain, the server trusts requests that carry valid session cookies, and the database trusts every SQL string it receives. An attacker doesn’t fight those systems — they use them as intended, with inputs those systems weren’t designed to reject.

Understanding how each attack actually works — the mechanics, the target, and what the attacker gains — changes how you read your own code. I’m going to walk through each one from the attacker’s side, and show you the code patterns that signal a vulnerability before any exploit runs.

On this page

The attacker’s first move: follow the inputs

An attacker approaching an unfamiliar web application isn’t looking for zero-days. They’re asking one question: where does user-supplied data go, and does the application trust it without checking?

Every web application has the same anatomy — form fields, URL parameters, cookies, HTTP headers, file uploads, API bodies. Each one is a potential input. The attacker methodically traces where those inputs land: in the HTML the browser renders, in a database query the server constructs, in an action the server performs on behalf of an authenticated user. Recognizing which context an input reaches is the core of the reconnaissance step.

The three attacks in this post each target one of those output contexts. XSS targets the rendered HTML. CSRF targets the server-side actions an authenticated browser triggers. SQL injection targets the database queries the server builds. Knowing which context applies tells the attacker which technique to reach for. It can tell you the same thing when reviewing your own code — more on that in the web security basics post, which covers the full attack surface picture.

XSS: the script that looks like it belongs

Cross-site scripting (XSS) works because browsers trust scripts that appear to originate from a page’s own domain. If an attacker can get their JavaScript into your HTML — whether directly in the response or stored in your database — the browser runs it with the same permissions as any script you shipped intentionally.

There are three approaches, and what differs is how the payload gets there.

Reflected XSS is the quickest to probe. The attacker finds a URL parameter or form field whose value ends up rendered in the page without encoding — a search query displayed as “You searched for: [input]”, or an error message that echoes the path. They test whether injecting markup into that parameter produces output the browser parses as code rather than text. If it does, they craft a link containing the payload and trick a logged-in user into visiting it. The browser reflects the script back from the server and executes it.

Stored XSS is more valuable and more patient. The attacker finds an input that gets saved to a database and later rendered for other users: a comment field, a username, a product review. They inject a script that runs for every user who views that content. Admin panels that display raw user submissions are a common target — the attacker’s payload executes in the browser of whoever reviews it, which is often an account with elevated privileges.

DOM-based XSS happens entirely client-side. The attacker finds JavaScript that reads a value from the URL fragment, localStorage, or postMessage and writes it directly into the DOM without sanitization. No server round-trip needed.

In all three cases, the immediate target is usually the session cookie. A script running on your domain can read document.cookie and forward it to a server the attacker controls. With that cookie, the attacker authenticates as you — the server sees a valid session token and has no reason to doubt it. That’s session hijacking, and it doesn’t require knowing your password.

Cookie theft is the most direct gain, but it’s not the only one. The injected script can read page content, silently fire API requests as the victim, inject fake login prompts, or redirect the user to a phishing page. A stored XSS payload on a high-traffic page makes the entire active user base a target. An admin account that encounters it is a full application compromise.

Check this before moving on

  • Any input that ends up in your HTML goes through encoding — not just trimming or length-checking
  • Your server-side templates auto-escape output in all contexts (HTML body, attributes, script blocks, CSS)
  • You know exactly which parts of your app bypass framework escaping, and why

CSRF: turning the browser into an unwitting accomplice

CSRF exploits a different trust entirely: the server’s trust in requests that arrive with valid session cookies. Browsers attach cookies automatically to every request sent to the origin domain, regardless of which page triggered that request.

The attacker’s setup: a web page they control contains a form or a script that fires a request to your application. When a logged-in user visits that page, their browser sends the request — complete with the session cookie your server uses to verify identity. The server can’t distinguish a forged request from a legitimate one. It sees a valid cookie and acts on the request.

The key limit of this attack is that the attacker can’t read the response. They’re not after data; they’re after actions. CSRF targets state-changing operations: fund transfers, password resets, email address changes, account deletions, privilege escalations. Anything the authenticated user can trigger, the attacker can trigger on their behalf — as long as the server relies solely on cookies to verify intent.

A GET request that performs a state change is the highest-severity case. An attacker can embed a zero-pixel image in an email:

<img src="https://your-app.com/account/delete?confirm=true" width="0" height="0" />

The browser loads the “image,” fires an authenticated GET, and the action completes. The user sees nothing. That’s why state-changing actions should only respond to POST, and why POST alone isn’t a full defense either.

For POST endpoints, the attacker uses a form with hidden fields that auto-submits on page load:

<form action="https://your-app.com/settings/email" method="POST">
  <input type="hidden" name="email" value="attacker@example.com" />
</form>
<script>document.forms[0].submit();</script>

The request fires when the victim loads the page, carries their valid cookies, and updates their account email. The attacker now controls account recovery.

One thing worth knowing: the same-origin policy does block cross-origin requests with custom headers or Content-Type: application/json. Modern APIs that require JSON bodies are partially protected — unless the CORS configuration is misconfigured to allow the attacker’s origin. If you’ve seen Access-Control-Allow-Origin: * paired with Access-Control-Allow-Credentials: true, you’ve seen one way that protection gets removed. The rate limiting, CORS, and security headers post covers why that combination is dangerous.

CSRF doesn’t bypass authentication — it exploits it. The attack works precisely because the user is authenticated. That’s what makes it counterintuitive: the more securely a user is logged in, the more powerful the forged request.

SQL injection: breaking out of the data lane

SQL databases don’t distinguish between the query structure a developer wrote and the data a user supplied. They execute SQL strings. If an attacker can change the structure of that string by controlling part of its content, they can change what the query does — not just which rows it returns, but whether it reads, writes, or destroys.

The classic starting point is a login form. The server constructs a query by concatenating user input directly into the SQL string:

SELECT * FROM users WHERE username = '[input]' AND password = '[input]'

The attacker supplies ' OR '1'='1 as the username. The query becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'

The OR '1'='1' condition is always true. The query returns the first matching row — often the admin account. Authentication bypassed, no password required.

That’s the tautology technique. Beyond getting in, the attacker has three more moves.

UNION-based enumeration extracts data from tables the query was never meant to touch. If the endpoint returns visible output — search results, product listings — the attacker appends a UNION SELECT clause to route data from other tables into that same response. A user table’s usernames and password hashes can appear inline with the application’s normal content.

Stacked queries (supported by some database engines, including SQL Server) let the attacker append a second statement after a semicolon. The first statement runs normally. The second can drop tables, modify records, or create new users. The trailing comment (--) discards anything left of the original query after the injection point.

Blind SQL injection works without visible output. If the application’s response differs even slightly based on whether the query returned results — a redirect, an error, a timing delay — the attacker can ask yes/no questions about the database. “Is the first character of the admin password greater than ‘m’?” Repeat enough times and the full schema and all the data follows, one bit at a time.

The attacker’s potential gains span the full range: reading every row in every table, modifying account records, deleting tables, and in some database configurations, reading files from the server’s filesystem.

What vulnerable code looks like before any exploit runs

An attacker reviewing source code (or probing outputs) looks for a small set of patterns. These are the same patterns an automated scanner runs against your whole application, continuously, in seconds.

For XSS, the signals are:

  • User input rendered without encoding: string concatenation into HTML, server templates that don’t auto-escape, innerHTML set from a variable
  • Explicit escaping bypasses: dangerouslySetInnerHTML, Angular’s bypassSecurityTrustHtml, [innerHTML] bindings
  • Client-side code reading location.hash, URL.searchParams, or document.referrer and writing the result into the DOM

For CSRF, the signals are:

  • State-changing endpoints that respond to GET requests
  • POST endpoints with no origin check, no CSRF token, and no SameSite attribute on the session cookie
  • CORS configuration that allows credentials from any origin

For SQL injection, the signal is nearly always string construction:

// Vulnerable: user input goes directly into the query string
const query = `SELECT * FROM orders WHERE user_id = ${userId} AND status = '${status}'`;
db.query(query);

The attacker doesn’t need sophisticated tools to find this. A grep for template literals or string concatenation near .query(, .execute(, or .raw( finds most of it. The defense — parameterized queries — is covered in detail in the post on preventing XSS, CSRF, and SQL injection.

See your app the way an attacker does

The three attacks in this post share an underlying pattern: they each turn the application’s own mechanisms against it. XSS makes the browser execute code the browser was told to trust. CSRF makes the server process actions the server was told to authenticate. SQL injection makes the database run queries the database was told to accept.

Implementing the defenses without understanding this pattern is how gaps appear. You encode HTML in the page body but miss a JavaScript string context. You add CSRF tokens to forms but leave the JSON API unprotected. You parameterize the main query but pass unsanitized input to an ORDER BY clause.

The more useful habit is to read your own code from the attacker’s starting point: where does this input go, and does the code that receives it treat it as inert data or as something the runtime will interpret? That question, applied while writing rather than after auditing, catches most of what the three attacks in this post depend on.

Three attack vectors — a script injection path from the top left, a browser-loop path from the top right, and a vertical database injection path from the bottom — all pointing inward toward a central hexagonal target representing a web application.

Each attack exploits a different trust relationship: the browser’s trust in scripts from its origin, the server’s trust in cookie-authenticated requests, and the database’s trust in the SQL it receives.

Sources