Injection, XSS, CSRF, broken auth, misconfig & secure coding
A regularly updated list of the 10 most critical web application security risks. Published by the Open Web Application Security Project. Standard awareness document for developers and security teams.
Users acting beyond their permissions. Includes: accessing other users' data by changing IDs, privilege escalation, CORS misconfiguration, forced browsing to admin pages. #1 risk in OWASP 2021.
# Vulnerable โ direct object reference
GET /api/users/42/orders # attacker changes to 43
# Fix โ verify ownership
@app.get("/api/users/{id}/orders")
def get_orders(id, current_user):
if id != current_user.id and not current_user.is_admin:
raise HTTPException(403)
return db.get_orders(id)Sensitive data exposed due to weak/missing encryption. Includes: plaintext passwords, weak algorithms (MD5, SHA1), missing TLS, hardcoded keys, insufficient key management.
# BAD
password_hash = md5(password) # weak hash
conn = http://api.example.com # no TLS
SECRET_KEY = "hardcoded-in-source" # exposed
# GOOD
password_hash = bcrypt.hash(password, rounds=12)
conn = https://api.example.com # TLS
SECRET_KEY = os.environ['SECRET_KEY'] # from envUntrusted data sent as part of a command or query. SQL injection, NoSQL injection, OS command injection, LDAP injection. Always use parameterized queries, never string concatenation.
# VULNERABLE โ SQL injection
query = f"SELECT * FROM users WHERE name = '{user_input}'"
# Input: ' OR '1'='1 โ dumps all users
# SAFE โ parameterized query
cursor.execute(
"SELECT * FROM users WHERE name = %s",
(user_input,)
)Flaws in architecture and design, not implementation bugs. Missing threat modeling, insecure business logic, no rate limiting on sensitive operations. Fix: threat model early, secure design patterns.
# Insecure design examples:
- No rate limit on password reset โ brute force
- No purchase limit โ bot buying all stock
- Security questions as only 2FA
- No re-auth for sensitive changes
# Fix: design-level controls
- Rate limiting on all auth endpoints
- CAPTCHA on registration
- Re-authentication for profile changes
- Threat modeling during design phaseDefault credentials, unnecessary features enabled, missing security headers, verbose error messages, unpatched systems. Most common in cloud, containers, and frameworks.
# Common misconfigs:
- Default admin:admin credentials
- Directory listing enabled
- Stack traces in production errors
- Unnecessary ports open
- Missing security headers
# Security headers to set:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000Using libraries, frameworks, or software with known vulnerabilities. Includes: outdated dependencies, unmaintained packages, not monitoring CVEs. Automate dependency scanning.
# Check for vulnerabilities
npm audit # Node.js
pip-audit # Python
bundle audit # Ruby
# Automate with CI/CD
- GitHub Dependabot
- Snyk
- OWASP Dependency-Check
# Keep updated
npm update
pip install --upgrade packageWeak passwords allowed, no brute-force protection, session IDs in URL, missing MFA, credential stuffing attacks. Previously called 'Broken Authentication'.
# Vulnerabilities:
- Permits weak passwords (password123)
- No account lockout after failed attempts
- Session token in URL parameters
- Missing multi-factor authentication
# Fixes:
- Enforce strong passwords + check breached lists
- Rate limit login attempts
- Secure session management (HttpOnly, Secure cookies)
- Implement MFA (TOTP, WebAuthn)Code or data from untrusted sources without verification. Includes: insecure CI/CD pipelines, auto-update without signature checks, insecure deserialization, compromised dependencies.
# Vulnerable โ unsigned updates
update_url = "http://example.com/update.zip"
# No integrity check, HTTP not HTTPS
# Secure โ verify integrity
import hashlib
expected_hash = "a1b2c3..."
actual_hash = hashlib.sha256(data).hexdigest()
assert actual_hash == expected_hash
# Use SRI for CDN scripts
<script src="cdn.js"
integrity="sha384-abc123..."
crossorigin="anonymous">Insufficient logging of security events, no alerting on attacks, logs not protected. Attackers average 200+ days undetected. Log auth events, access control failures, input validation failures.
# What to log:
- Login attempts (success + failure)
- Access control failures
- Input validation failures
- Server-side errors
- Admin actions
# Log format (structured)
{"timestamp": "2025-01-15T10:30:00Z",
"level": "WARN",
"event": "auth.login_failed",
"ip": "1.2.3.4",
"user": "admin",
"reason": "invalid_password"}Attacker tricks the server into making requests to internal resources. Can access metadata services, internal APIs, or private networks. Always validate and whitelist destination URLs.
# Vulnerable โ user controls URL
@app.post("/fetch")
def fetch_url(url: str):
return requests.get(url).text
# Attacker: url=http://169.254.169.254/metadata
# โ leaks cloud credentials!
# Fix โ validate and whitelist
ALLOWED_HOSTS = ["api.example.com"]
def fetch_url(url: str):
parsed = urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("Host not allowed")Injecting malicious scripts into web pages viewed by others. Stored XSS (in DB), Reflected XSS (in URL), DOM XSS (client-side). Escape output, use CSP, sanitize HTML.
# Vulnerable
<p>Welcome, {{ user_input }}</p>
# Input: <script>document.cookie</script>
# Fixes:
# 1. Escape output
<p>Welcome, {{ user_input | escape }}</p>
# 2. Content Security Policy
Content-Security-Policy: script-src 'self'
# 3. HttpOnly cookies (JS can't read)
Set-Cookie: session=abc; HttpOnly; SecureAttacker tricks authenticated user into making unwanted requests. User's browser sends cookies automatically. Prevent with CSRF tokens, SameSite cookies, and checking Origin/Referer headers.
# Attack: hidden form on evil site
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>
# Fixes:
# 1. CSRF token in forms
<input type="hidden" name="csrf" value="random-token">
# 2. SameSite cookie
Set-Cookie: session=abc; SameSite=StrictUse parameterized queries (prepared statements) everywhere. Use ORMs. Validate input types. Apply least-privilege database permissions. Never build queries with string concatenation.
# Python โ parameterized
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# Node.js โ parameterized
db.query('SELECT * FROM users WHERE id = $1', [userId])
# ORM (SQLAlchemy)
User.query.filter_by(id=user_id).first()
# Stored procedure
CALL get_user_by_id(?)Security headers instruct browsers to enable protections. Essential ones: HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy.
# Essential security headers
Strict-Transport-Security: max-age=63072000; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()Hash with bcrypt, scrypt, or Argon2 (adaptive work factor). Never MD5/SHA1. Salt is included automatically. Never store plaintext. Use breach detection (HaveIBeenPwned API).
import bcrypt
# Hash password (salt auto-generated)
hashed = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
)
# Verify
bcrypt.checkpw(
password.encode('utf-8'),
hashed
) # True/False
# NEVER:
# md5(password), sha1(password), plaintextCSP tells browsers which sources of content are allowed. Prevents XSS by blocking inline scripts and unauthorized sources. Start with report-only mode, then enforce.
# Strict CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
report-uri /csp-report
# Report-only (testing)
Content-Security-Policy-Report-Only: ...Use strong signing algorithms (RS256, ES256, not HS256 with weak secrets). Short expiry (15-30 min). Store in HttpOnly cookies, not localStorage. Validate issuer, audience, and expiry.
# Secure JWT practices:
- Algorithm: RS256 or ES256 (asymmetric)
- Expiry: 15-30 minutes
- Refresh: separate long-lived refresh token
- Storage: HttpOnly, Secure, SameSite cookie
- Validate: iss, aud, exp, iat
# NEVER:
- Store in localStorage (XSS vulnerable)
- Use 'none' algorithm
- Skip expiry validation
- Put sensitive data in payloadGive every user, process, and system only the minimum permissions needed. Applies to database users, API keys, IAM roles, file permissions. Reduces blast radius of breaches.
# Database โ per-service users
CREATE USER api_readonly;
GRANT SELECT ON orders TO api_readonly;
-- NOT: GRANT ALL PRIVILEGES
# AWS IAM โ scoped policies
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/uploads/*"
}
-- NOT: "Action": "s3:*", "Resource": "*"A WAF inspects HTTP traffic and blocks malicious requests using rulesets. It operates at Layer 7 and can detect SQLi, XSS, and other injection patterns. WAFs complement โ but don't replace โ secure coding practices.
# AWS WAF rule examples:
# Block SQL injection
aws wafv2 create-rule-group \
--name "SQLi-Protection" \
--rules '[{
"Statement": {
"SqliMatchStatement": {
"FieldToMatch": { "Body": {} },
"TextTransformations": [
{ "Priority": 0, "Type": "URL_DECODE" },
{ "Priority": 1, "Type": "HTML_ENTITY_DECODE" }
]
}
},
"Action": { "Block": {} }
}]'
# WAF deployment options:
# AWS WAF โ ALB, CloudFront, API GW
# Cloudflare โ DNS-based, edge proxy
# ModSecurity โ open-source, Apache/Nginx
# OWASP CRS โ core rule set for ModSec