Security
Password hashing with bcrypt, authentication vs authorization, JWTs (structure, signing, expiry), environment variables for secrets, SQL injection defense via parameterized queries, XSS and auto-escaping, CSRF tokens, and API security essentials.
Writing code that works is only half the job — writing code that's safe from common attacks is just as important, especially for anything handling user data, passwords, or payments. This file covers the security fundamentals every Python developer should know, with an emphasis on defensive best practices.
1. What is Secure Coding?
What is it?
Secure coding means writing software with an awareness of common vulnerabilities, and deliberately avoiding the mistakes that let attackers exploit them.
Definition: Secure coding is the practice of writing software in a way that protects it against vulnerabilities, unauthorized access, and data breaches.
Why do we use it?
A single security mistake — a leaked password, an unvalidated input, a hardcoded secret — can expose an entire application's data, damage user trust, and in many cases carry legal consequences. Security isn't an optional extra feature; it's a core responsibility of professional software development.
2. Password Hashing
What is it?
Password hashing converts a password into an irreversible, scrambled string before storing it — so that even if a database is compromised, the actual passwords aren't directly exposed.
Definition: Hashing is a one-way transformation that converts data into a fixed-length string, from which the original data cannot be feasibly recovered.
Why NEVER Store Plain-Text Passwords
If a database storing plain-text passwords is ever breached, every single user's actual password is immediately exposed — and since many people reuse passwords across sites, this puts their other accounts at risk too.
Simple Example — Secure Password Hashing with bcrypt
bashpip install bcrypt
pythonimport bcrypt password = "MySecurePassword123" # Hashing a password (done once, when the user creates their account) hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) print(hashed) # a long, scrambled byte string - safe to store in the database # Verifying a password (done every time the user logs in) entered_password = "MySecurePassword123" is_correct = bcrypt.checkpw(entered_password.encode(), hashed) print(is_correct) # True
Explanation of the Code
bcrypt.gensalt()generates a random "salt" — extra random data mixed into the hash, ensuring that even two identical passwords produce different hashes, protecting against certain precomputed-hash attacks.bcrypt.hashpw()performs the actual one-way hashing — there's no way to reverse this back into the original password.bcrypt.checkpw()checks a freshly entered password against the stored hash — the application never needs to (and never should) store or compare plain-text passwords directly.
Common Mistakes
- Storing passwords in plain text, or using a fast, general-purpose hash (like plain MD5 or SHA-256) instead of a slow, purpose-built password-hashing algorithm like
bcrypt— general-purpose hashes are designed to be fast, which actually makes them easier to brute-force for passwords specifically. - Reusing the same salt for every password, which defeats much of the purpose of salting.
Important Points
- Always use a dedicated password-hashing library (
bcrypt, or similar) — never a general-purpose hash function, and never plain text. - Password verification should compare hashes, never decrypt anything back to plain text (proper hashing is one-way and cannot be reversed at all).
3. Authentication vs Authorization
What is it?
- Authentication — verifying who someone is (e.g., checking a username and password).
- Authorization — verifying what an already-authenticated person is allowed to do.
Definition: Authentication confirms a user's identity. Authorization determines what actions or resources that authenticated user is permitted to access.
Real-World Example
Logging into a company system with your employee credentials is authentication. Whether you, as a regular employee, can access the admin-only payroll settings (versus an actual administrator) is authorization.
Important Points
- These are two distinct steps, often confused — a system can correctly authenticate a user's identity while still correctly denying them authorization for a specific action.
4. JWT (JSON Web Tokens)
What is it?
A JWT is a compact, digitally signed token commonly used to represent an authenticated user's identity, passed along with API requests — letting a server verify who's making a request without needing to look up a session in a database on every single call.
Definition: A JWT (JSON Web Token) is a signed, self-contained token used to securely transmit information — typically for authentication — between parties.
Structure of a JWT (Conceptual)
A JWT has three parts, separated by dots: header.payload.signature
- Header — metadata about the token (e.g., which signing algorithm was used).
- Payload — the actual data (e.g., user ID, expiration time) — note this is only encoded, not encrypted, so it shouldn't contain secret information.
- Signature — a cryptographic signature, generated using a secret key, that proves the token hasn't been tampered with.
Simple Example — Creating and Verifying a JWT
bashpip install pyjwt
pythonimport jwt import datetime SECRET_KEY = "your-secret-key-here" # in real apps, load this from an environment variable # Creating a token (typically done at login) payload = { "user_id": 42, "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1) } token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") print(token) # Verifying a token (typically done on every subsequent request) try: decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) print(decoded) # {'user_id': 42, 'exp': ...} except jwt.ExpiredSignatureError: print("Token has expired") except jwt.InvalidTokenError: print("Invalid token")
Explanation of the Code
jwt.encode()creates the signed token, including an expiration time (exp), so it automatically becomes invalid after a set period.jwt.decode()verifies the token's signature (proving it wasn't tampered with) and checks it hasn't expired, before returning the original payload data.
Common Mistakes
- Storing sensitive data (like a plain-text password) directly in a JWT's payload — remember, the payload is only encoded, not encrypted, and can be read by anyone who has the token.
- Not setting an expiration time, creating tokens that remain valid indefinitely if ever leaked or stolen.
- Hardcoding the
SECRET_KEYdirectly in source code (see Environment Variables, below).
Important Points
- JWTs are signed (tamper-proof), but not encrypted by default — never put secret information in the payload.
- Always set a reasonable expiration time on tokens.
5. Environment Variables
What is it?
Environment variables store configuration values (like API keys, database passwords, and secret keys) outside of your actual source code — so secrets never end up committed to version control or visible to anyone reading the code.
Simple Example
bash# In a .env file (never committed to Git - add it to .gitignore!) DATABASE_PASSWORD=supersecretpassword123 API_KEY=abcd1234efgh5678
pythonimport os from dotenv import load_dotenv load_dotenv() # loads variables from a .env file into the environment database_password = os.environ.get("DATABASE_PASSWORD") api_key = os.environ.get("API_KEY") print(api_key) # abcd1234efgh5678
bashpip install python-dotenv
Explanation of the Code
load_dotenv()reads a local.envfile and makes its contents accessible viaos.environ, without those actual secret values ever appearing directly in your Python source code..envshould always be listed in.gitignore(from the Git & GitHub file), so it's never accidentally committed and pushed to a public repository.
Common Mistakes
- Committing a
.envfile to Git, or hardcoding secrets directly in.pyfiles. - Using
os.environ["KEY"](which raises an error if missing) instead ofos.environ.get("KEY")(which safely returnsNoneif missing) when a default/fallback is acceptable.
Important Points
- Never hardcode secrets (passwords, API keys, tokens) directly in source code.
- Always add
.envto.gitignore.
6. SQL Injection (Recap and Defense)
What is it?
SQL injection is a vulnerability where an attacker manipulates a database query by injecting malicious input into a field that gets inserted directly into SQL — a topic already introduced in the Database Programming file.
The Defense — Parameterized Queries
pythonimport sqlite3 connection = sqlite3.connect("app.db") cursor = connection.cursor() username = input("Enter username: ") # SAFE: the database driver handles the value separately from the query structure cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
Explanation
- Using
?placeholders (or%sin some other database libraries) ensures user input is always treated strictly as data, never as part of the SQL command's actual structure — completely preventing this class of attack. - Never build a SQL query by directly inserting user input into a string (via f-strings,
.format(), or plain concatenation).
Important Points
- Parameterized queries are the standard, complete defense against SQL injection — always use them for any query involving user-supplied input.
- This applies to every database library covered in the Database Programming file (
sqlite3, MySQL/PostgreSQL connectors, and ORMs like SQLAlchemy handle this automatically).
7. Cross-Site Scripting (XSS)
What is it?
XSS is a vulnerability where untrusted user input is displayed on a webpage without being properly handled, potentially letting malicious script content execute in other users' browsers.
Definition: Cross-Site Scripting (XSS) is a vulnerability that allows malicious script content to be injected into web pages viewed by other users, typically due to improperly handled user input.
The Defense — Escaping Output
Modern web frameworks' templating engines (like Flask's Jinja2, covered in the Web Development file) automatically escape output by default, converting special characters into their safe, literal display form rather than executable content.
pythonfrom flask import render_template_string user_comment = "<script>alert('test')</script>" # Jinja2 automatically escapes this safely by default: render_template_string("<p>{{ comment }}</p>", comment=user_comment) # Renders literally as text on the page, NOT as an executable script
Important Points
- Always rely on your framework's built-in auto-escaping for displaying user-provided content — avoid manually disabling it (Jinja2's
|safefilter, for instance) unless you fully understand and control the content's origin. - Never trust user input to be "just text" — always assume it could contain something unexpected.
8. Cross-Site Request Forgery (CSRF)
What is it?
CSRF is a vulnerability where a malicious site tricks a logged-in user's browser into unknowingly submitting a request to a different site where they're authenticated — potentially performing an action (like changing account settings) without the user's actual intent.
The Defense — CSRF Tokens
Web frameworks defend against this by including a unique, secret CSRF token in every form, which the server verifies matches before processing the request — a malicious external site has no way to know or guess this token.
python# Flask-WTF example (conceptual) from flask_wtf import FlaskForm class UpdateProfileForm(FlaskForm): # FlaskForm automatically includes CSRF protection pass
Important Points
- Modern web frameworks (Django, Flask with Flask-WTF) provide built-in CSRF protection — enable and use it rather than building your own from scratch.
9. API Security Essentials
What is it?
Beyond the specific vulnerabilities above, a few general practices keep APIs secure:
| Practice | Why |
|---|---|
| Always use HTTPS | Encrypts data in transit, preventing eavesdropping |
| Rate limiting | Prevents abuse (e.g., brute-force login attempts, denial-of-service) |
| Input validation | Rejects malformed or unexpected data before it's processed |
| Least privilege | Each component/user should have only the access it genuinely needs |
| Keep dependencies updated | Older library versions may contain known, publicly documented vulnerabilities |
Simple Example — Input Validation
pythondef create_user(username, age): if not username or len(username) > 50: raise ValueError("Invalid username") if not isinstance(age, int) or age < 0 or age > 150: raise ValueError("Invalid age") # proceed only after validation passes
Explanation: Validating input before using it anywhere important (queries, calculations, file operations) is one of the simplest and most effective defensive habits — many vulnerabilities ultimately stem from trusting input that should have been checked first.
Common Beginner Mistakes — Summary for This Section
- Storing passwords in plain text, or using a fast general-purpose hash instead of
bcrypt. - Hardcoding secrets (API keys, passwords, JWT secret keys) directly in source code.
- Building SQL queries with string formatting instead of parameterized queries.
- Disabling a framework's automatic output escaping without good reason.
- Skipping input validation, trusting that user input will always be well-formed.
Cheat Sheet — Security
python# Password hashing import bcrypt hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) bcrypt.checkpw(entered_password.encode(), hashed) # Environment variables import os from dotenv import load_dotenv load_dotenv() secret = os.environ.get("SECRET_KEY") # JWT import jwt token = jwt.encode({"user_id": 1, "exp": expiry}, SECRET_KEY, algorithm="HS256") jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) # SQL injection prevention cursor.execute("SELECT * FROM table WHERE col = ?", (value,)) # NEVER f-string queries
Interview Questions
Q1. Why should passwords never be stored in plain text? Answer: If the database is ever breached, every user's actual password would be immediately exposed, putting not just that application but potentially other services (due to password reuse) at risk.
Q2. What is the difference between authentication and authorization? Answer: Authentication verifies who a user is (identity). Authorization determines what that authenticated user is permitted to do or access.
Q3. What is a JWT, and is its payload encrypted? Answer: A JSON Web Token is a signed, self-contained token used to represent authenticated identity. Its payload is encoded (readable by anyone with the token) and signed (tamper-proof), but not encrypted by default — sensitive data should never be placed directly in a JWT payload.
Q4. How do parameterized queries prevent SQL injection? Answer: They keep user-supplied input strictly separate from the SQL command's structure, so input is always treated as data, never as executable SQL syntax.
Q5. What is XSS, and how do modern frameworks typically defend against it? Answer: Cross-Site Scripting is a vulnerability allowing malicious script content to be injected into pages viewed by other users. Modern templating engines defend against it with automatic output escaping, converting special characters into safe, literal display text by default.
Q6. Why should secrets (API keys, passwords) be stored in environment variables instead of source code? Answer: Hardcoded secrets can be accidentally exposed if the code is shared, committed to a public repository, or seen by unauthorized people — environment variables (loaded from a .env file excluded via .gitignore) keep secrets separate from the codebase itself.
Practice Questions
Beginner
- Hash a password using
bcryptand verify it correctly against both a correct and an incorrect attempt. - Create a
.envfile with a sample API key, and load it into a Python script usingpython-dotenv. - Write a function that validates a username (non-empty, under 50 characters) before "creating" a user.
- Explain, in your own words, the difference between authentication and authorization, with your own example.
- Rewrite an f-string-based SQL query (unsafe) as a parameterized query (safe).
Intermediate
- Create and verify a JWT with a 30-minute expiration time, and demonstrate what happens when you try to decode an expired token.
- Write an input validation function for a signup form that checks username, email format (using regex from the Regular Expressions file), and age.
- Explain, in your own words, why storing a JWT secret key directly in source code is a security risk.
- Research and briefly explain (in comments) what "rate limiting" means and why login endpoints specifically benefit from it.
- Explain, using your own words, why general-purpose hash functions like plain SHA-256 are considered unsuitable for password hashing compared to
bcrypt.
Challenge
- Build a simple user registration and login system (using the Database Programming file's SQLite patterns) that hashes passwords with
bcryptand never stores plain text. - Extend the login system to issue a JWT upon successful login, and write a function that verifies the JWT before allowing access to a "protected" action.
- Write a short security review (as comments) of the Student Management System mini project from the Database Programming file, identifying at least 3 security improvements it would need before being production-ready (e.g., password hashing, input validation, parameterized queries already in place).