How web apps get attacked and the standard defences. Browser-side controls (CSP, headers, cookies) are in 3. HTTP Security Headers and CSP. Principles are in 1. Security Fundamentals.
OWASP Top 10 at a glance
The OWASP Top 10 is the standard awareness list for web risks (2021 edition shown; a newer edition exists, so check owasp.org).
| # | Risk | One-line defence |
|---|---|---|
| A01 | Broken access control | Check permissions server-side on every object and action |
| A02 | Cryptographic failures | TLS everywhere, strong hashing, no secrets in plain text |
| A03 | Injection (SQL, command, XSS) | Parameterised queries, output encoding, validate input |
| A04 | Insecure design | Threat model early, secure patterns, abuse cases |
| A05 | Security misconfiguration | Hardened defaults, no debug in production, remove samples |
| A06 | Vulnerable and outdated components | Track and patch dependencies |
| A07 | Identification and authentication failures | MFA, rate limits, safe sessions |
| A08 | Software and data integrity failures | Signed artifacts, pinned dependencies, safe deserialisation |
| A09 | Logging and monitoring failures | Log security events, alert on them |
| A10 | Server-side request forgery (SSRF) | Allow-list outbound targets, block metadata addresses |
Injection
Untrusted data is interpreted as code. The fix is to keep data and commands separate.
# Vulnerable: user input becomes part of the SQL
cur.execute(f"SELECT * FROM users WHERE name = '{name}'")
# Safe: parameterised query
cur.execute("SELECT * FROM users WHERE name = %s", [name])
- Use the ORM or parameterised queries; never build SQL by string concatenation
- Command injection: avoid the shell; pass an argument list (
subprocess.run([...], shell=False)) and validate input - Give the application's database user only the rights it needs (no
DROP, no superuser)
Cross-Site Scripting (XSS)
Attacker-controlled script runs in a victim's browser, so it can read the page, act as the user and steal tokens.
| Type | How |
|---|---|
| Stored | Malicious input saved, then shown to others (comments, profiles) |
| Reflected | Input echoed straight back (search term in the page) |
| DOM-based | Client-side JavaScript writes untrusted data into the DOM |
Defences, in order:
1. Encode output for its context (HTML, attribute, JavaScript, URL). Django templates auto-escape; do not bypass with |safe or mark_safe on user data
2. Avoid innerHTML, document.write, eval; use textContent
3. Sanitise any HTML you must allow with a vetted library (for example nh3 or DOMPurify)
4. Add a Content Security Policy as the safety net: 3. HTTP Security Headers and CSP
5. Mark session cookies HttpOnly
Cross-Site Request Forgery (CSRF)
A malicious site makes the victim's browser send an authenticated request (the cookie rides along).
- Use anti-CSRF tokens on state-changing requests (Django
{% csrf_token %},CsrfViewMiddleware) SameSite=LaxorStrictcookies block most cross-site sends- State changes use
POST/PUT/DELETE, neverGET - APIs using
Authorization: Bearerheaders rather than cookies are not CSRF-prone, but must then protect the token from XSS
Authentication and Sessions
- Hash passwords with Argon2id or bcrypt; Django's default PBKDF2 is acceptable, Argon2 is better
- Rate-limit and lock out repeated failures; use the same message for "unknown user" and "wrong password"
- MFA for sensitive roles; short-lived reset links, single use
- Sessions: random unguessable IDs,
Secure+HttpOnly+SameSitecookies, rotate the ID on login, expire on logout and idle timeout - JWTs: verify the signature and the algorithm, check
exp,aud,iss; keep them short-lived; do not put secrets in the payload (it is only encoded, not encrypted) - OAuth2 / OIDC: use the authorisation-code flow with PKCE; never roll your own login against a provider
Broken Access Control
The most common serious bug. Examples:
- IDOR: /invoices/1042 works for any logged-in user. Always check the object belongs to the requester
- Forced browsing to /admin or hidden URLs
- Role checks only in the front end
- Missing checks on API methods (GET protected, DELETE not)
Fix: deny by default, one central authorisation layer, test with two different users, and log denied attempts.
SSRF
The server is tricked into fetching a URL the attacker picks, often reaching internal services or the cloud metadata endpoint (169.254.169.254) to steal credentials.
- Allow-list the hosts you may call; resolve DNS and block private / link-local ranges
- Do not return raw responses to the user
- On AWS use IMDSv2 (session-token based metadata) and restrict the instance role: 2. AWS IAM and Security
File Uploads, Deserialisation and Others
| Risk | Defence |
|---|---|
| Unsafe uploads | Check type by content, limit size, rename, store outside the web root or in object storage, never execute |
| Insecure deserialisation | Do not unpickle / deserialise untrusted data; use JSON with schema validation |
| Open redirects | Redirect only to allow-listed or relative URLs |
| Path traversal | Normalise paths; never join raw user input to a file path |
| XXE | Disable external entities in XML parsers |
| Clickjacking | frame-ancestors / X-Frame-Options: 3. HTTP Security Headers and CSP |
| Mass assignment | Whitelist the fields a request may set |
API Security
- Authenticate and authorise every endpoint; per-object checks
- Rate limit and set request size limits
- Validate input against a schema; return generic errors (no stack traces)
- Version and retire old endpoints; do not expose debug / admin APIs
- Do not leak internal IDs, emails or excessive fields in responses
Django Production Checklist
$ python manage.py check --deploy
DEBUG = False
ALLOWED_HOSTS = ["example.com"]
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # never in git
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
Also: keep the admin off a guessable public path or behind a VPN, use django-axes or rate limiting for logins, and keep Django and packages patched.
Firewall tuning and false positives: 6. WAF in Production.
Testing
- SAST reads source (Bandit for Python, Semgrep, CodeQL)
- DAST attacks a running app (OWASP ZAP). Only against systems you own or have permission to test
- Passive scanning of headers, cookies and TLS: Siteward Web Security Scanner
- Add tests for access control (user A cannot read user B's data)
- Wire these into CI: 5. Secure Coding and Supply Chain