Application security · Intermediate
Finding and fixing the OWASP Top 10
Why it matters
Application security appeared in 30% of the Cybersecurity Engineer job descriptions we read, and secure coding is part of every serious engineering role. The OWASP Top 10 lists the most common web application weaknesses; a few of them, above all injection and broken access control, cause a large share of real breaches. The good news is that the fixes are well understood.
The concept
The most common classes and their fixes:
Injection (SQL, OS command, LDAP): untrusted input is treated as code. Fix with parameterised queries and by passing arguments as lists, never by building strings.
Broken access control (including insecure direct object references): the server does not check that the caller may act on the object. Fix by checking ownership and permissions on the server for every request.
Cryptographic failures: weak hashing (MD5, SHA-1) for passwords, unencrypted sensitive data. Fix with modern password hashing and authenticated encryption (see the Security fundamentals shelf).
Security misconfiguration and vulnerable dependencies: covered on the Vulnerability management and DevOps shelves.
Static analysis (SAST) reads code and flags risky patterns. It has false positives and misses logic flaws, so it complements, and never replaces, secure design and review. Run it in CI so new issues are caught on every change.
Worked example: a module with three flaws
import hashlib
import sqlite3
import subprocess
def find_user(conn: sqlite3.Connection, username: str):
query = "SELECT id, role FROM users WHERE username = '" + username + "'"
return conn.execute(query).fetchone()
def password_hash(password: str) -> str:
return hashlib.md5(password.encode()).hexdigest()
def run_backup(target: str) -> None:
subprocess.call("tar czf backup.tgz " + target, shell=True)Bandit flags each one:
$ pip install bandit
$ bandit -f custom insecure.py
B608 MEDIUM line 7 Possible SQL injection through string-based query construction
B324 HIGH line 12 Use of weak MD5 hash for security
B602 HIGH line 16 subprocess call with shell=True identified, security issueNow the fixed version, where input can never become code:
import hashlib
import shlex
import sqlite3
import subprocess
def find_user(conn: sqlite3.Connection, username: str):
# Parameterised query: the driver keeps input as data, never as SQL.
return conn.execute("SELECT id, role FROM users WHERE username = ?", (username,)).fetchone()
def password_hash(password: str, salt: bytes) -> str:
# A slow, salted KDF for passwords (Argon2 is better still; see the security shelf).
return hashlib.scrypt(password.encode(), salt=salt, n=2**14, r=8, p=1).hex()
def run_backup(target: str) -> None:
# No shell: arguments are passed as a list, so input cannot inject commands.
subprocess.run(["tar", "czf", "backup.tgz", target], check=True)
def _demo() -> None:
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER, username TEXT, role TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'asha', 'user')")
# A classic injection string is now treated as a (non-matching) username, not as SQL.
hostile = "asha' OR '1'='1"
print("injection attempt returns:", find_user(conn, hostile))
print("real username returns:", find_user(conn, "asha"))
print("shlex.quote example:", shlex.quote("a b; rm -rf /"))$ python3 -c "import secure; secure._demo()"
injection attempt returns: None
real username returns: (1, 'user')
shlex.quote example: 'a b; rm -rf /'
$ bandit secure.py
(no medium or high severity issues)The classic injection string asha' OR '1'='1 now returns nothing, because the parameterised query looks for a user literally named that. The password uses a slow, salted key derivation function instead of MD5, and the backup runs without a shell, so a target such as a b; rm -rf / is passed as one argument, not interpreted by a shell. (Bandit still notes low-severity, informational items about subprocess; those are not vulnerabilities, but you document why the call is safe.)
Broken access control does not show up in a scanner, because the code "works". Guarding it is a design habit: for an endpoint such as GET /invoices/{id}, load the invoice and check it belongs to the authenticated user before returning it, or an attacker just increments the id to read other people's data.
Common mistakes
Trusting client-side checks. Hiding a button does not stop a crafted request; enforce on the server.
Escaping by hand. Use parameterised queries and libraries; hand-rolled escaping is always incomplete.
Treating SAST output as the whole job. It misses access-control and business-logic flaws, which need review and threat modelling.
Fixing one instance. Search the codebase for the same pattern everywhere.
Interview angle
"What is SQL injection and how do you prevent it?" (parameterised queries and least-privilege database accounts). "What is an insecure direct object reference?" (acting on an id without checking ownership). "Where does SAST fit, and what does it miss?" Be ready to spot the flaw in a short code sample and write the fix.
Practice task
Add a
GET /invoices/{id}handler that is vulnerable to an insecure direct object reference, then fix it with an ownership check and a test proving one user cannot read another's invoice.Add Bandit (or Semgrep) to a CI workflow so medium and high findings fail the build.