The Web Application Attack Surface
How a Web Application Works
A web application is, in essence, a conversation between a client (the browser) and a server over the HTTP protocol. The browser sends a request and the server replies with a response. Each request includes a method (GET, POST, PUT, DELETE), a URL, a set of headers, and optionally a body with data. The response includes a status code (200, 404, 500), headers, and usually a body of HTML, JSON, or other content.
The key fact for security is that HTTP is a stateless protocol: the server remembers nothing between one request and the next. Each request is independent and is interpreted on its own. This means that any information an attacker wants to manipulate travels somewhere in the request: the URL, the query string parameters, the headers, the cookies, or the body. If your application blindly trusts any of that data, a vulnerability is born right there.
It helps to picture the anatomy of a real request. A raw HTTP request looks like this:
POST /api/checkout HTTP/1.1
Host: shop.example.com
Cookie: session=8f3b2a...; theme=dark
Content-Type: application/json
Authorization: Bearer eyJhbGci...
{ "productId": 42, "quantity": 2 }
Every line in that block is an input channel controlled by the client: the path, each header, each cookie, and the entire JSON body. The server has no way of knowing whether those values were produced by your legitimate interface or crafted by hand with a tool. That is why we say every input is a potential attack point: the application's security depends on what the server does with that data, not on how the browser was supposed to produce it.
Cookies, Sessions and State
Since HTTP is stateless, applications need a mechanism to recognize a user across requests. The most common solution is session cookies. After logging in, the server generates a random session identifier, stores it, and sends it to the browser via the Set-Cookie header. On every subsequent request the browser returns that cookie, and the server uses it to recover the user's state.
A huge attack surface appears here. If the session identifier is predictable, an attacker can guess it. If it travels unencrypted, it can be intercepted on a hostile network. If the cookie lacks the HttpOnly, Secure, and SameSite attributes, it becomes exposed to theft through malicious JavaScript or to CSRF attacks. Many historical breaches boil down to exactly this: session cookies without HttpOnly that an injected script read and exfiltrated, letting the attacker fully impersonate the victim without ever knowing their password.
Let's look at the most common mistake. A session cookie issued without security attributes is vulnerable:
// VULNERABLE: session cookie with no protection
app.post('/login', (req, res) => {
const sessionId = createSession(req.body.user);
// No HttpOnly, no Secure, no SameSite:
// readable by JavaScript, travels over plain HTTP, sent cross-site
res.setHeader('Set-Cookie', `session=${sessionId}`);
res.json({ ok: true });
});
The secure version explicitly declares the attributes that shrink the attack surface:
// SECURE: the cookie is out of reach of JavaScript and plain HTTP
app.post('/login', (req, res) => {
const sessionId = createSession(req.body.user);
res.cookie('session', sessionId, {
httpOnly: true, // unreadable from document.cookie -> stops XSS theft
secure: true, // only sent over HTTPS -> stops interception
sameSite: 'lax', // not sent on cross-site requests -> mitigates CSRF
maxAge: 1000 * 60 * 60, // bounded session lifetime
});
res.json({ ok: true });
});
HttpOnly prevents document.cookie from reading the value, so an injected script cannot steal the session. Secure guarantees the cookie never travels over plain HTTP. SameSite controls whether the cookie is attached to requests originating from other sites, which is the basis of CSRF protection. Understanding this lifecycle well is the foundation for reasoning about session hijacking or session fixation, which we will cover in the authentication lesson.
The Client-Server Trust Model
One of the most important security principles is: never trust the client. Everything that happens in the browser is under the user's control. JavaScript validations, hidden form fields, prices displayed on a page, or disabled menus can be trivially modified with the browser DevTools or an intercepting proxy.
This implies that any security control must be enforced on the server. Client-side validation is useful for user experience, but it is not a security measure. An attacker can bypass the browser entirely and send hand-crafted requests straight to the backend. All authorization logic, data validation, and business rules must live on the server, where the user cannot tamper with them.
The classic case is an endpoint that trusts a sensitive value sent by the client. This checkout code trusts the price and the role arriving in the request body:
// VULNERABLE: the server trusts data the client controls
app.post('/api/checkout', (req, res) => {
const { productId, price, role } = req.body;
// The client can send price: 0 or role: 'admin'
if (role === 'admin') applyAdminDiscount();
chargeCard(req.user, price); // price dictated by the attacker
createOrder(productId, price);
res.json({ charged: price });
});
A malicious user simply changes price to 0 or role to admin in the request. The secure version ignores that data and derives it on the server from trusted sources:
// SECURE: the server recomputes the price and derives the role from its own source
app.post('/api/checkout', async (req, res) => {
const { productId, quantity } = req.body;
// Validate the shape and type of the input
if (!Number.isInteger(productId) || !Number.isInteger(quantity) || quantity < 1) {
return res.status(400).json({ error: 'Invalid input' });
}
// The price comes from the database, never from the client
const product = await db.products.findById(productId);
if (!product) return res.status(404).json({ error: 'Product not found' });
// The role comes from the server-side session, not the request body
const role = await getUserRole(req.session.userId);
const unitPrice = role === 'admin' ? product.adminPrice : product.price;
const total = unitPrice * quantity;
await chargeCard(req.session.userId, total);
await createOrder(productId, quantity, total);
res.json({ charged: total });
});
The pedagogical difference is key: the client may request to buy a product, but the price and the role are decided by the server by consulting sources the attacker does not control. That is the heart of the trust model.
Mapping the Attack Surface
The attack surface is the set of all points where an attacker can attempt to introduce or extract data. In a typical web application it includes every URL parameter, every form field, every HTTP header, every cookie, every API endpoint, every file that can be uploaded, and every integration with external services.
To understand an application's security posture it is essential to enumerate this surface systematically. Security professionals use an intercepting proxy like Burp Suite or OWASP ZAP, which sits between the browser and the server and lets you view and modify every request and response. This reveals hidden parameters, undocumented endpoints, and data flows that are not visible from the interface. The browser DevTools (Network and Application tabs) complement this work by showing headers, cookies, and background calls. The larger the attack surface, the more care you must take to protect every input, which is why minimizing the surface — removing dead endpoints, disabling unused HTTP methods, not exposing unnecessary fields — is itself a defense.
These tools are legitimate and are used both in authorized assessments and in development itself: running your application behind a proxy during testing shows you exactly what data flows in and out, and often exposes undue trust in the client that was not obvious from the code.
Defense in Depth as a Framework
No single defense is enough. The principle of defense in depth consists of applying multiple layers of security control, so that if one fails, another stops the attack. In practice this means combining server-side input validation, output encoding, robust authentication, strict access control, encrypted transport with HTTPS/TLS, secure configuration, and monitoring. If an attacker evades an endpoint's validation, the HttpOnly cookie limits the damage; if they steal a session, role-based access control halts the escalation. Each layer assumes the previous ones might fail.
Throughout this course we will use the OWASP Top 10 as a map of the most critical risks. It is a community-maintained list that compiles the most frequent and impactful vulnerability categories in web applications. Each vulnerability we study also maps to a CWE (Common Weakness Enumeration) identifier, the standard catalog of software weaknesses. Having this mental framework clear will let you reason in a structured way about any application you need to protect.
Prevention Checklist
- Treat all client input (URL, query string, headers, cookies, body) as untrusted and validate it on the server before using it.
- Never trust prices, roles, permissions, or identities sent by the client: derive or recompute them in the backend from trusted sources (database, session).
- Remember that browser-side JavaScript validation is only for UX; always duplicate that validation on the server.
- Issue session cookies with
HttpOnly,Secure, andSameSite, and with a bounded lifetime. - Enforce HTTPS/TLS on all traffic so cookies and credentials never travel in the clear.
- Use long session identifiers generated with a cryptographically secure generator.
- Minimize the attack surface: remove dead endpoints, disable unused HTTP methods, and do not expose unnecessary fields or parameters.
- Systematically enumerate all your inputs and endpoints, using proxies like Burp Suite or OWASP ZAP and the DevTools.
- Apply defense in depth: do not rely on a single control; layer validation, authentication, authorization, and monitoring.
- Use the OWASP Top 10 and CWE identifiers as a framework to reason about and prioritize risks in a structured way.