Secure coding — input validation, output encoding, fail secure
Writing functional code is different from writing secure code. Most well-known vulnerabilities — SQL Injection, XSS, Path Traversal — originate from three simple failures: trusting user input, mixing data with code in output, and letting the application expose internal information when something goes wrong.
Input Validation
Every external input is potentially malicious. This includes form fields, query strings, HTTP headers, cookies, uploaded files, and even data from third-party APIs.
Practical rules:
- Whitelist, not blacklist. Define what is accepted; reject the rest.
- Validate type, size, and format before any processing.
- Never rely on client-side validation alone. JavaScript can be disabled or bypassed.
import re
def validate_username(username: str) -> bool:
# Only allows letters, digits and underscore, 3-32 chars
pattern = r'^[a-zA-Z0-9_]{3,32}$'
return bool(re.match(pattern, username))
# Bad: accepts any string
# Good: restricts to expected format
if not validate_username(username):
raise ValueError("Invalid username")
Output Encoding
Data rendered in output must be encoded for the context where it appears. The same piece of data may require different encodings depending on the destination.
| Context | Risk | Solution |
|---|---|---|
| HTML body | XSS | HTML entity encoding |
| HTML attribute | XSS | Attribute encoding |
| JavaScript | XSS | JS string encoding |
| SQL | SQL Injection | Prepared statements |
| Filesystem | Path traversal | Sanitize and resolve path |
# Bad — direct concatenation in SQL
query = f"SELECT * FROM users WHERE name = '{name}'"
# Good — prepared statement
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
<!-- Bad — raw data in HTML -->
<p>{{ user.bio }}</p>
<!-- Good — escaped by the template engine (Jinja2, etc.) -->
<p>{{ user.bio | e }}</p>
Fail Secure
When something goes wrong, the application should deny access by default, not grant it. And it should never leak internal information.
# Bad — exception exposes database details
@app.route("/user/<id>")
def get_user(id):
user = db.query(f"SELECT * FROM users WHERE id={id}")
return jsonify(user)
# Good — catch exception, log internally, return generic response
@app.route("/user/<id>")
def get_user(id):
try:
user = db.get_user_by_id(id)
if user is None:
return jsonify({"error": "Not found"}), 404
return jsonify(user.to_dict())
except Exception:
logger.exception("Error fetching user id=%s", id)
return jsonify({"error": "Internal server error"}), 500
Error messages shown to users must be generic. Details belong in server-side logs only.
Principle of Least Privilege in Code
Code should operate with the minimum permissions required for the task:
- Database connection: user with SELECT/INSERT only, not DBA.
- File reading: open as read-only when writing is not needed.
- Process: do not run as root unless strictly necessary.
Quick Checklist
- All input validated via whitelist before use
- Prepared statements in every query
- Output escaped for the correct context
- Errors logged internally, generic message to the client
- Dependencies up to date (no known CVEs)
- No secrets hardcoded in the codebase