Application security
Lesson 2 of 4
2Application security
The big pictureMost breaches need no genius. They need an unchecked input, a URL where the attacker changes a number and sees someone else's data, or an old library with a known flaw.
What the picture shows: The line of defence for every request: validate input, then authorise, then a safe query, then escape on output.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Your home is safe not because of one strong lock but because of habits: you lock every door, you do not give the key to just anyone, you ask who is knocking before opening, and you fix a broken window at once. Code security is similar habits repeated on every request.
OWASP Top 10: (1) broken access control, like IDOR when a user changes /invoices/7 to 8; the fix is checking ownership on the server for every resource. (2) Cryptographic failures: passwords with bcrypt, argon2 or scrypt plus a salt, never bare MD5 or SHA256. (3) Injection: always parameterised queries, and no eval or shell with input. XSS: escape on output and a CSP. CSRF: CSRF tokens and SameSite cookies. Also: misconfiguration (DEBUG in production), vulnerable components (pip-audit, npm audit, Dependabot), and logging and monitoring to spot attacks. And rate-limit sign-in.
Before You Start
🗄️SQL
You write parameterised queries.
🔐HTTPS and encryption
The DNS and encryption lesson in the internet track.
The Core Idea
1Injection: attack your own code
The same query twice: once by joining text, once with parameters. The malicious input breaks the first and fails against the second:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (name TEXT, is_admin INTEGER)")
db.executemany("INSERT INTO users VALUES (?, ?)", [("sara", 0), ("admin", 1)])
attack = "nobody' OR '1'='1"
unsafe = f"SELECT name FROM users WHERE name = '{attack}'"
print("string-built :", db.execute(unsafe).fetchall())
safe = "SELECT name FROM users WHERE name = ?"
print("parameterised:", db.execute(safe, (attack,)).fetchall())2Passwords
Never store the password or a fast hash of it. Use a deliberately slow function with a random salt per user, so if the database is stolen guessing becomes very expensive:
import hashlib
import hmac
import os
def hash_password(password, salt=None):
salt = salt or os.urandom(16)
digest = hashlib.scrypt(password.encode(), salt=salt, n=2**14, r=8, p=1)
return salt, digest
def verify(password, salt, digest):
_, candidate = hash_password(password, salt)
return hmac.compare_digest(candidate, digest)
salt, stored = hash_password("correct horse battery")
print("right password:", verify("correct horse battery", salt, stored))
print("wrong password:", verify("123456", salt, stored))
_, other = hash_password("correct horse battery")
print("same password, same hash for two users?", other == stored)hashlib.sha256(password) because it is "encryption". SHA256 is very fast, so an attacker tries billions of passwords a second. A slow salted hash is the standard.3Access control
The most dangerous and simplest flaw: the UI hides the delete button from non-owners, but the server does not check. The attacker sends the request directly. Always check on the server:
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user = Depends(current_user), db = Depends(get_db)):
invoice = db.get(Invoice, invoice_id)
if invoice is None or invoice.owner_id != user.id:
raise HTTPException(status_code=404) # 404, not 403: do not confirm it exists
return invoiceWorked example
Problem: a booking app shows /bookings/1043. A security tester changed the number to 1042 and saw another patient's name and phone. Cause: the route loads the booking by id alone. Fix: WHERE id = ? AND patient_id = ? with a 404 for non-owners, plus an automated test requesting another user's booking. Extra lesson: log refused access attempts so you can spot someone walking the numbers.
Myth vs Reality
| Myth | Reality |
|---|---|
| HTTPS makes the app secure. | It protects only the road. Injection and IDOR pass through HTTPS just fine. |
| Hiding the button in the UI is enough. | Attackers do not use your UI; they send requests directly. |
Test your understanding
Which approach prevents SQL injection?
Parameters make input data, never code.
How do you store passwords?
Slowness and salt make guessing expensive.
A user changed the invoice number in the URL and saw someone else's. What flaw is it?
The server did not check who owns the resource.
Quick review cards
Check Your Understanding
Why 404 instead of 403 for a resource the user does not own?
So you do not confirm to an attacker that the resource exists.
What is the risk of DEBUG in production?
Error pages reveal code, settings and sometimes secrets.
How do you know a library you use has a flaw?
pip-audit, npm audit or Dependabot compare your versions with vulnerability databases.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs2
Done with this lesson?
Sign in to save your progress.