Skip to main content

Overview

Security is not optional. Understanding security fundamentals helps you build systems that protect user data and resist attacks. The 2017 Equifax breach — which exposed 147 million people’s Social Security numbers — happened because of a single unpatched Apache Struts vulnerability that had a fix available for two months. The 2013 Target breach (40 million credit cards stolen) started with compromised HVAC vendor credentials. Security failures are rarely sophisticated; they are almost always a known vulnerability that someone failed to address. The good news: most security problems have well-established solutions. You do not need to be a cryptographer — you need to consistently apply known best practices.

Authentication vs Authorization

Authentication (AuthN)

Who are you?
  • Verifies identity
  • Login, passwords, MFA
  • “Prove you are who you claim”

Authorization (AuthZ)

What can you do?
  • Verifies permissions
  • Roles, policies, ACLs
  • “Are you allowed to do this?”

Authentication Methods

Password-Based (Traditional)

Never store passwords in plaintext — not even “temporarily,” not even in logs, not even in a dev database. If your database is breached (and statistically, it will be), properly hashed passwords buy your users time to change their credentials. The algorithm matters: use bcrypt, scrypt, or Argon2 — these are intentionally slow to make brute-force attacks computationally expensive. Fast hashes like MD5 or SHA-256 can be cracked at billions of attempts per second on modern GPUs.

JWT (JSON Web Tokens)

Practical tip: JWTs cannot be revoked once issued (they are stateless). If a user logs out or their account is compromised, the token remains valid until it expires. To handle this, use short-lived access tokens (15 minutes) paired with longer-lived refresh tokens stored server-side, which CAN be revoked.

OAuth 2.0 Flow

OWASP Top 10 (2021)

The OWASP Top 10 is the industry-standard awareness document for web application security risks, updated every few years based on data from hundreds of organizations. Broken Access Control moved from #5 to #1 in the 2021 edition because it is the most commonly exploited vulnerability class — and the easiest to prevent.

1. Broken Access Control

The most dangerous category because it lets attackers access other users’ data or perform admin actions. Also called IDOR (Insecure Direct Object Reference) — the attacker simply changes a user ID in the URL and gets someone else’s data.

2. Cryptographic Failures

Using the wrong cryptographic algorithm is as dangerous as using none at all. MD5 was broken in 2004, SHA-1 in 2017. An attacker with a modern GPU can compute 10 billion MD5 hashes per second — meaning any MD5-hashed password under 8 characters can be cracked in under a minute.

3. Injection

SQL injection has been the #1 or #2 vulnerability for over 20 years, and it is still being discovered in production systems today. The attack is trivial: if user_id is 1; DROP TABLE users; --, your f-string builds a query that deletes your entire users table. Parameterized queries make this attack structurally impossible because the database treats parameters as data, never as executable SQL.

4. Insecure Design

Insecure design is a category new to the 2021 OWASP list. It recognizes that some security flaws are architectural — no amount of perfect implementation can fix a fundamentally insecure design. Security questions (“What is your mother’s maiden name?”) are a classic example: the answers are often public knowledge on social media, making them trivially guessable.

5. Security Misconfiguration

The most common misconfiguration is leaving debug mode enabled in production. Debug mode typically exposes stack traces, source code, environment variables, and sometimes an interactive debugger — giving attackers a complete blueprint of your system. Django’s debug page, for instance, shows all settings including database credentials.

6. Vulnerable Components

7. Authentication Failures

Without rate limiting, an attacker can try millions of password combinations against your login endpoint. Even with bcrypt, a bot making 1,000 requests per second can test common passwords across thousands of accounts. Rate limiting and account lockout are the first line of defense.
Practical tip: Implement both IP-based and account-based rate limits. IP-based alone fails against distributed botnets (thousands of IPs). Account-based alone allows attackers to lock out legitimate users (denial of service). Together they provide layered defense.

Encryption

Symmetric vs Asymmetric

HTTPS/TLS

CORS (Cross-Origin Resource Sharing)

CORS is a browser security mechanism that controls which websites can make requests to your API. Without CORS restrictions, a malicious website could make API requests on behalf of your logged-in users (using their cookies). The allow_origins: ["*"] wildcard effectively disables this protection — any website on the internet can make authenticated requests to your API.

Content Security Policy (CSP)

CSP is the most powerful defense against XSS (Cross-Site Scripting). It tells the browser: “Only execute JavaScript from these specific sources. Block everything else.” Even if an attacker manages to inject a script tag into your page, the browser will refuse to execute it because the script source is not in your CSP whitelist. GitHub’s CSP alone prevented thousands of potential XSS attacks in its first year of deployment.

API Security Best Practices

Rate Limiting

API Key Authentication

Security Headers

Security headers are cheap insurance — a few lines of configuration that block entire categories of attacks. Adding these headers takes 10 minutes and prevents clickjacking, MIME confusion attacks, protocol downgrade attacks, and more. Use securityheaders.com to scan your site and verify all headers are properly set.

Secrets Management

Environment Variables (Basic)

The simplest secrets management approach: store secrets in environment variables, never in code. This is the bare minimum — suitable for development and simple deployments, but for production systems with multiple services, consider a dedicated secrets manager (Vault, AWS Secrets Manager) that provides audit logging, automatic rotation, and fine-grained access control.

HashiCorp Vault (Production)

Vault solves three problems that environment variables cannot: (1) audit logging — you know exactly which service accessed which secret and when, (2) dynamic secrets — database credentials are generated on-the-fly and automatically expire, so there is no long-lived password to steal, and (3) centralized rotation — change a secret in one place, all services pick up the new value.

Penetration Testing Checklist

  • Test for default credentials
  • Test password policy enforcement
  • Test account lockout mechanism
  • Test session timeout
  • Test “remember me” functionality
  • Test password reset flow
  • Test MFA bypass attempts
  • Test horizontal privilege escalation (user A accessing user B’s data)
  • Test vertical privilege escalation (user becoming admin)
  • Test IDOR (Insecure Direct Object References)
  • Test missing function level access control
  • Test API endpoint authorization
  • Test SQL injection (all input fields)
  • Test NoSQL injection
  • Test Command injection
  • Test LDAP injection
  • Test XPath injection
  • Test template injection (SSTI)
  • Test Reflected XSS
  • Test Stored XSS
  • Test DOM-based XSS
  • Test CSRF protection
  • Test Clickjacking protection
  • Test WebSocket security

Security Checklist

  • Validate all user inputs server-side
  • Use allowlists, not blocklists
  • Sanitize before output (prevent XSS)
  • Validate file uploads (type, size, content)
  • Use parameterized queries (prevent SQLi)
  • Hash passwords with bcrypt/Argon2 (cost factor ≥12)
  • Implement MFA for sensitive operations
  • Use secure session management (HttpOnly, Secure, SameSite cookies)
  • Rate limit login attempts
  • Implement secure password reset (time-limited tokens)
  • Consider passwordless authentication (WebAuthn)
  • Implement least privilege principle
  • Check permissions server-side (never trust client)
  • Use role-based access control (RBAC)
  • Audit access to sensitive resources
  • Implement proper multi-tenancy isolation
  • Encrypt sensitive data at rest (AES-256)
  • Use HTTPS everywhere (TLS 1.2+)
  • Never log sensitive data (passwords, tokens, PII)
  • Implement proper key management and rotation
  • Use secure random number generation
  • Implement data retention and deletion policies
  • Keep systems and dependencies updated
  • Use WAF (Web Application Firewall)
  • Implement network segmentation
  • Enable audit logging
  • Regular security scanning and penetration testing
  • Implement DDoS protection

Common Vulnerabilities Quick Reference

This table covers the vulnerabilities you will encounter most frequently in real-world applications. For each, the prevention is well-established — the challenge is consistently applying it across every endpoint, every input, every dependency.
Security is everyone’s job. Don’t assume “someone else will handle security.” Build it in from the start. Security is not a feature — it is a requirement. The cost of fixing a security vulnerability increases 100x from design to production. A parameterized query takes 5 seconds to write; cleaning up after a SQL injection breach takes months and millions of dollars.
Interview Tip: Be prepared to discuss real security incidents you’ve handled, how you stay updated on security threats, and your experience with security tools and practices. A strong answer sounds like: “We discovered an IDOR vulnerability during a code review where user IDs were sequential integers. We implemented UUIDs for external identifiers, added authorization middleware to all data-access endpoints, and set up automated SAST scanning in our CI pipeline to prevent regression.” Concrete incidents and specific remediation steps beat abstract security knowledge.

Interview Deep-Dive

Strong Answer:
  • This is a Severity 1 incident. The exposed secret means anyone who saw it can forge valid JWT tokens for any user in your system, including admin accounts. The blast radius is total authentication compromise.
  • Step 1 (immediate, within 15 minutes): Rotate the JWT secret. Generate a new secret and deploy it to all services that verify JWTs. This instantly invalidates every existing token — every user gets logged out. This is inconvenient but necessary. Do not delay rotation to “minimize user impact” — every minute the old secret is active, an attacker can forge tokens.
  • Step 2 (within 1 hour): Scrub the secret from Git history. Simply deleting the file and committing does not help — git log preserves it forever. Use BFG Repo-Cleaner or git filter-repo to rewrite history and remove the secret from all commits. Force-push the cleaned history. If the repo is public, assume the secret was already scraped by automated credential harvesting bots (services like GitHub’s secret scanning, TruffleHog, and Gitrob exist specifically for this).
  • Step 3 (within 4 hours): Audit for unauthorized access. Check your access logs for the past 3 days for suspicious patterns: logins from unusual IPs or geolocations, privilege escalation attempts, bulk data access, or API calls that do not match normal user behavior. If any forged tokens were used, determine what data was accessed and prepare for disclosure.
  • Step 4 (within 24 hours): Add guardrails to prevent recurrence. (1) Enable GitHub’s built-in secret scanning (or a pre-commit hook like detect-secrets) that blocks commits containing known secret patterns. (2) Move JWT secrets to a secrets manager (Vault, AWS Secrets Manager) so they are never in code or environment files. (3) Implement JWT key rotation capability — use asymmetric keys (RS256) with a JWKS endpoint so you can rotate keys without redeploying every service.
  • Step 5: Conduct a blameless postmortem. How did the secret end up in the repo? Was it a .env file that should have been in .gitignore? A configuration file that was not excluded from version control? Fix the systemic issue, not just the symptom.
Follow-up: You mentioned switching from HS256 to RS256 for JWT signing. Why is that an improvement, and what are the trade-offs?HS256 (HMAC) uses a single shared secret for both signing and verification. Every service that needs to verify a token must have the secret, which means more copies of the secret exist and the blast radius of a compromise is larger. RS256 (RSA) uses a private key for signing and a public key for verification. Only the auth service holds the private key; all other services use the public key, which is safe to distribute publicly. If a consuming service is compromised, the attacker gets the public key — which is useless for forging tokens. The trade-off: RS256 signatures are computationally more expensive than HS256 (roughly 10-100x slower for signing, 5-10x slower for verification). For most applications processing thousands of requests per second, this is negligible. But for extremely high-throughput systems (millions of verifications per second), the CPU cost matters and you may want ES256 (ECDSA), which offers asymmetric security with faster verification than RSA. The other advantage of RS256: you can publish a JWKS (JSON Web Key Set) endpoint and rotate keys by adding a new key to the set and removing the old one. Services automatically pick up the new key without redeployment.
Strong Answer:
  • IDOR is the most common access control vulnerability (OWASP #1 for a reason), and patching one endpoint is a game of whack-a-mole. The fix must be systemic.
  • Immediate fix for the reported endpoint: Add an authorization check that verifies the authenticated user owns the requested resource. In pseudocode: if order.user_id != current_user.id and not current_user.is_admin: raise 403. This is a 3-line fix.
  • Systemic fix layer 1: Replace sequential integer IDs with UUIDs for all external-facing identifiers. Sequential IDs (/orders/1, /orders/2, /orders/3) invite enumeration — an attacker writes a loop from 1 to 10 million. UUIDs (/orders/a1b2c3d4-...) are unguessable, adding a layer of defense-in-depth. This does not replace authorization checks (security through obscurity is not security), but it eliminates casual enumeration.
  • Systemic fix layer 2: Implement an authorization middleware or decorator that runs on every data-access endpoint. Instead of trusting each developer to remember the authorization check, make it automatic. In a Python/FastAPI application, create a dependency that resolves the resource AND verifies ownership in one step: def get_order(order_id, current_user) -> Order either returns the order (if the user owns it) or raises 403. Every endpoint uses this dependency instead of querying the database directly.
  • Systemic fix layer 3: Automated testing. Add authorization tests to the CI pipeline that attempt to access resources as the wrong user and assert 403 responses. Use a DAST (Dynamic Application Security Testing) tool like OWASP ZAP in the pipeline that automatically tests for IDOR by replaying authenticated requests with different user sessions.
  • Systemic fix layer 4: Default-deny architecture. Instead of each endpoint opting IN to authorization, make authorization the default. Every endpoint requires a policy definition, and if no policy exists, the endpoint returns 403. This ensures new endpoints are secure by default rather than vulnerable by default.
Follow-up: Your authorization middleware works for simple ownership checks. But what about shared resources — an order that involves a buyer AND a seller, both of whom should access it? How do you model this?This is where you need ABAC (Attribute-Based Access Control) rather than simple ownership checks. The policy becomes: “A user can access an order if they are the buyer, the seller, or an admin.” I would model this as a set of access rules per resource type. For the Order resource: can_access(user, order) = user.id == order.buyer_id OR user.id == order.seller_id OR user.role == 'admin'. For more complex scenarios (a support agent who can view orders from their assigned customers), I would use a policy engine like OPA (Open Policy Agent) or Casbin that evaluates policies defined in a declarative language. The policy definitions live outside the application code — product managers can update access rules without a code deployment. The key design principle: never scatter authorization logic across endpoint handlers. Centralize it in a policy layer that is independently testable, auditable, and changeable.
Strong Answer:
  • Session-based auth: the server creates a session record (in Redis or a database) on login, returns a session ID as a cookie. Every subsequent request sends the cookie, the server looks up the session, and retrieves the user context. The session is the source of truth, stored server-side.
  • JWT-based auth: the server creates a signed token containing the user’s identity and claims, returns it to the client. Every subsequent request sends the token, the server verifies the signature and reads the claims. No server-side lookup needed — the token IS the session.
  • Session-based advantages: (1) Revocation is instant — delete the session record and the user is logged out immediately. (2) Smaller payload per request — a 32-byte session ID versus a 500-byte JWT. (3) Server has full control — you can change user permissions and it takes effect on the next request.
  • JWT-based advantages: (1) Stateless — no shared session store needed, which simplifies horizontal scaling and microservices (any service can verify the token independently). (2) Works well for cross-domain and mobile — no cookie dependency. (3) Reduces database load — no session lookup on every request.
  • JWT-based disadvantages that most people underestimate: (1) You cannot revoke a JWT before it expires. If a user’s account is compromised, their token remains valid for its entire lifetime. Workarounds (blocklists, short-lived tokens + refresh tokens) add back the server-side state that JWTs were supposed to eliminate. (2) JWTs grow with claims — adding roles, permissions, and metadata makes them large. (3) Token theft is more dangerous than session theft because the JWT contains everything needed to impersonate the user, while a session ID requires access to the session store.
  • My choice depends on the architecture. For a traditional monolithic web application: session-based auth with Redis. Simpler, more secure, and the “stateless” benefit of JWT is irrelevant when you have one server. For a microservices architecture with multiple domains and mobile clients: short-lived JWTs (15 minutes) + server-side refresh tokens (30 days, revocable, stored in a database). This gives you stateless verification for most requests while maintaining the ability to revoke access.
Follow-up: An engineer on your team wants to store user roles and permissions inside the JWT payload to avoid a database lookup on every request. What are the risks?The risk is stale authorization data. If an admin revokes a user’s admin role, the JWT still contains "role": "admin" until it expires. For a 24-hour token, that is 24 hours of unauthorized admin access. For a 15-minute token, the window is smaller but still exists. The trade-off comes down to your security requirements. For a blog platform, a 15-minute window of stale permissions is acceptable. For a financial system, it is not. My approach: store the minimal claim set in the JWT (user_id, token_id, expiry) and fetch permissions from a fast cache (Redis) on each request. The cache lookup adds ~1ms of latency — negligible — and you can update permissions instantly by updating the cache entry. This is a hybrid approach: you get JWT’s stateless verification for authentication (identity) while keeping authorization (permissions) in a mutable, revocable store.
Strong Answer:
  • Encryption in transit (HTTPS/TLS) protects data while it is moving between two parties — from the user’s browser to your server, or between two services. It prevents eavesdropping and man-in-the-middle attacks. An attacker sniffing network traffic sees encrypted gibberish instead of plaintext passwords and credit card numbers.
  • Encryption at rest protects data where it is stored — on disk, in a database, in backups, in S3 buckets. It prevents an attacker who gains access to the storage medium (stolen hard drive, compromised backup, misconfigured S3 bucket) from reading the data.
  • HTTPS alone is NOT sufficient. Here is why: HTTPS protects data between point A and point B. But data is vulnerable at rest at both endpoints. If your database is not encrypted and an attacker gets access to the database files (through a backup leak, a compromised server, or a misconfigured storage volume), they read everything in plaintext. The 2019 Capital One breach exposed 100 million customer records — the data was encrypted in transit but the attacker accessed the unencrypted data at rest through an SSRF vulnerability that reached the AWS metadata service.
  • Defense in depth requires both: (1) Encryption in transit: TLS 1.2+ everywhere, including internal service-to-service communication (mTLS in a service mesh). Do not assume your internal network is safe — zero-trust networking means encrypting everything. (2) Encryption at rest: enable storage-level encryption (AWS EBS encryption, RDS encryption, S3 default encryption). For sensitive fields (SSN, credit card numbers), add application-level encryption using envelope encryption — encrypt the data with a data key, encrypt the data key with a master key stored in AWS KMS or Vault. This way, even a database admin who can query the table sees ciphertext. (3) Encryption in use: for the most sensitive data, consider technologies like AWS Nitro Enclaves or homomorphic encryption that protect data even while it is being processed. This is emerging technology, not yet mainstream, but it is the frontier.
  • My response to the product manager: “HTTPS is essential but it is one layer. Think of it like locking the door to your house — necessary, but it does not help if someone breaks in through a window. We also need encryption at rest (safe inside the house), strong access controls (who has keys), and monitoring (security cameras). Security is layers, not a single checkbox.”
Follow-up: Your database stores user credit card numbers. The compliance team says you need PCI DSS compliance. How does this change your encryption approach?PCI DSS (Payment Card Industry Data Security Standard) changes everything about how you handle card data. First, the best approach: do not store credit card numbers at all. Use a payment processor like Stripe or Braintree that handles card storage and returns a token. Your system only stores the token, which is useless to an attacker. This dramatically reduces your PCI scope — instead of the full 300+ controls of PCI DSS Level 1, you fall under SAQ A, which is a fraction of the compliance burden. If you must store card numbers (rare, but some businesses require it): (1) Encrypt with AES-256 using unique keys per card (not one key for all cards). (2) Store encryption keys in a Hardware Security Module (HSM), never on the same server as the encrypted data. (3) Mask card numbers in all logs, error messages, and support tools (show only last 4 digits). (4) Segment your network so the card database is in an isolated VPC with strict access controls. (5) Implement key rotation — change encryption keys annually and re-encrypt all data. The cost difference is stark: full PCI DSS compliance for a system that stores cards costs $50K-500K annually in audits and infrastructure. Outsourcing to Stripe costs a percentage per transaction but eliminates nearly all of that compliance burden.