- Write server-side input validation based on an allow-list
- Apply least privilege to databases and services
- Move secrets out of code and act correctly after a leak
- Understand CVE, CVSS and dependency-audit tools
Most vulnerabilities come not from exotic tricks but from ordinary programmer habits: trusting input, running a service with admin rights, committing a password to a Git repository, a library that has not been updated for three years. In this lesson you will learn four habits that make your code safer every day.
1. Input validation: the allow-list
Every input is untrusted: forms, URL parameters, headers, cookies, uploaded files, even requests from your own mobile app (they can be modified). Validation must happen on the server; checks in the browser are only for the user's convenience. Check type, length, range and format, and describe what is valid instead of hunting for bad characters.
BAD = ['<script>', 'DROP TABLE', '--']
def is_safe(text):
return not any(bad in text for bad in BAD)
is_safe('<SCRIPT>') # True - uppercase slips throughimport re
def is_valid_username(text):
return re.fullmatch(r'[a-z0-9_]{3,16}', text) is not None
is_valid_username('<SCRIPT>') # Falseimport re
USERNAME = re.compile(r'[a-z0-9_]{3,16}')
def validate_signup(username, age):
errors = []
if not USERNAME.fullmatch(username):
errors.append('username: 3-16 of a-z 0-9 _')
if not (type(age) is int and 10 <= age <= 120):
errors.append('age: whole number 10-120')
return errors or ['OK']
for username, age in [('aysel_07', 15), ('Murad!', 16), ('elvin', -3), ('x', 200)]:
print(username.ljust(9), validate_signup(username, age))▸ Expected output
aysel_07 ['OK'] Murad! ['username: 3-16 of a-z 0-9 _'] elvin ['age: whole number 10-120'] x ['username: 3-16 of a-z 0-9 _', 'age: whole number 10-120']
fullmatch requires the whole string to match the format.2. The principle of least privilege
Every user, process and service should get only the rights its job needs, and only for as long as needed. A web app's database account should not be an administrator, services should not run as root or Administrator, and admin work needs a separate account. Even if the app is hacked, the blast radius stays small.
GRANT ALL PRIVILEGES ON *.* TO 'shop_app'@'%';GRANT SELECT, INSERT, UPDATE ON shop.orders TO 'shop_app'@'10.0.0.%';
GRANT SELECT ON shop.products TO 'shop_app'@'10.0.0.%';DROP DATABASE or read other databases, because it was never granted those rights.This principle has two close relatives. Secure defaults: a new account, file or API key starts with no permissions, and rights are added deliberately later. Fail closed: when a check ends in an error — for example, the authorisation server does not answer — the code must refuse access instead of letting the request through “just in case”.
3. Managing secrets
Passwords, API keys, tokens and private keys are never stored in code. Git history remembers even deleted files forever, and bots search public repositories for keys. Keep secrets in environment variables or a secrets manager (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault), add the .env file to .gitignore and run a secret scanner (for example gitleaks) before committing.
DB_PASSWORD = 'Baku2024!' # now in Git history forever
connect(user='shop_app', password=DB_PASSWORD)import os
DB_PASSWORD = os.environ['DB_PASSWORD'] # set by the server or a secrets manager
connect(user='shop_app', password=DB_PASSWORD)4. Vulnerabilities in dependencies
Most of a modern application's code is other people's libraries. A known vulnerability receives a CVE identifier and a CVSS score (0–10). Tools such as npm audit, pip-audit, Dependabot and OWASP Dependency-Check compare your dependencies with these databases. Lock files (package-lock.json, poetry.lock) pin exact versions, and an SBOM lists every component. Also watch out for fake packages with names similar to popular ones (typosquatting).
| CVSS score | Severity | What to do |
|---|---|---|
| 0.1–3.9 | Low | fix in the next planned update |
| 4.0–6.9 | Medium | fix within weeks |
| 7.0–8.9 | High | prioritise, fix within days |
| 9.0–10.0 | Critical | update at once or apply a temporary mitigation |
# JavaScript project: check dependencies against known vulnerabilities
npm audit
# Python project
pip install pip-audit
pip-auditWrite is_valid_quantity(text) for the “quantity” field of an order form: only strings that represent a whole number from 1 to 99 ([0-9] digits, 1–2 characters) return True. Spaces, a minus sign, 5e3 and characters like ² must be rejected, and the function must never crash.
import re
def is_valid_quantity(text):
# allow-list: 1-2 ASCII digits, value 1..99
return False
for value in ['3', '99', '0', '100', '-1', ' 7', '5e3', '²']:
print(repr(value), is_valid_quantity(value))▸ Expected output
'3' True '99' True '0' False '100' False '-1' False ' 7' False '5e3' False '²' False
Key points
- Validate every input on the server with an allow-list: type, length, range, format.
- Validation does not replace parameterised queries and escaping.
- Least privilege limits the damage a compromised component can do.
- Secrets live in the environment or a secrets manager, not in code; a leaked secret is rotated at once.
- Audit dependencies with tools, prioritise by CVSS score and keep an SBOM.
Check yourself
10 questions. Every correct answer earns XP.