Security Practices That Actually Protect Production Apps (After They've Been Attacked)
Part 2 of a practical production security series
Authentication and encryption don't stop scraping, brute force, spam, or slow-burn abuse.
I've seen production systems that were "secure" on paper quietly fall apart under real-world traffic — not from exotic zero-days, but from missing basics that everyone meant to add later.
In Part 1, we covered authentication, input validation, and encryption — the foundation. This article focuses on the infrastructure-level controls teams usually skip until something breaks in production.
TL;DR — What Actually Stops Abuse in Production
• Security headers that block entire attack classes
• Endpoint-specific rate limiting (not global limits)
• CORS locked down to real origins — never *
• Webhooks verified with HMAC and constant-time comparison
Where These Controls Live
┌──────────────┐
│ Browser │
└──────┬───────┘
│
┌──────▼───────┐
│ Edge / CDN │ ← Security headers, rate limiting
└──────┬───────┘
│
┌──────▼───────┐
│ API Gateway │ ← CORS, endpoint limits
└──────┬───────┘
│
┌──────▼───────┐
│ Application │ ← Auth, validation (Part 1)
└──────────────┘These controls live outside your business logic — which is exactly why they're so effective.
1. Security Headers: Your First (and Cheapest) Line of Defense
Most teams add security headers after a penetration test tells them to. That's backwards.
Security headers stop entire categories of browser-based attacks before your application code ever runs. They should be enforced globally via middleware, edge configuration, or your CDN.
Minimum Production Baseline
X-Content-Type-Options: nosniff
Prevents MIME-type sniffing attacks.
X-Frame-Options: DENY
Blocks clickjacking by preventing iframe embedding.
X-XSS-Protection: 1; mode=block
Enables built-in browser XSS filtering (legacy, but still helpful).
Strict-Transport-Security (HSTS)
Forces HTTPS and prevents protocol downgrade attacks.
Content-Security-Policy (CSP)
Restricts where scripts, styles, images, and frames can load from.
Referrer-Policy: strict-origin-when-cross-origin
Prevents leaking sensitive URLs to third parties.
Permissions-Policy
Disables access to dangerous browser features like camera, microphone, and geolocation.
Cache-Control: no-store
Ensures sensitive responses are never cached.
What This Looks Like in Practice
Before (common production response):
HTTP/1.1 200 OK
Content-Type: text/htmlAfter (locked-down baseline):
HTTP/1.1 200 OK
Content-Type: text/html
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=63072000; includeSubDomains
Content-Security-Policy: default-src 'self'
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=()
Cache-Control: no-storeThis isn't "extra security."
This is what modern browsers expect.
2. Rate Limiting: If You Don't Have This, You Don't Have Auth
If you don't rate-limit, you don't have authentication — you have a suggestion.
Rate limiting prevents brute force attacks, spam, and API abuse. The most common mistake teams make is applying global limits instead of endpoint-specific ones.
Practical Production Limits
Login: 5 attempts / 5 minutes
Signup: 3 attempts / 15 minutes
Refresh token: 10 attempts / 5 minutes
Health check: 60 requests / minute
Admin endpoints: 30 requests / minute
Track limits by IP address, and by user ID where possible.
Once limits are exceeded:
- Apply temporary lockouts (e.g., 15-minute ban)
- Log the event
- Return generic error messages (don't leak signal)
Make limits configurable so you can relax or disable them in development and testing.
Requests
▲
│ ██████████████ ← Attack traffic
│ ██████████████
│ ██████████████
│
│ █ █ █ █ █ █ █ █ █ █ ← Legitimate users
│
└───────────────────────────▶ Time
▲
Rate limit kicks inRate limiting isn't about stopping attackers completely.
It's about making abuse expensive and noisy.
3. CORS: The Difference Between "Accessible" and "Exploitable"
CORS controls which websites are allowed to call your API from a browser. Misconfigured CORS doesn't usually cause outages — it causes quiet exposure.
Production Rules That Should Be Non-Negotiable
Whitelist origins explicitly
Never use * in production.
Enable credentials only when needed
Set credentials: true only for cookie-based auth.
Restrict HTTP methods
Allow only what you support: GET, POST, PUT, PATCH, DELETE, OPTIONS.
Cache preflight responses
Set maxAge to ~1 hour to reduce unnecessary preflights.
A Real CORS Mistake
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueThis combination is invalid and dangerous.
Browsers will reject it — but not before you've created confusing, inconsistent behavior across environments.
Correct version:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueIf your API is public, be intentional.
If it's private, be strict.
4. Webhook Security: Trust Nothing That Hits Your Endpoint
Webhooks are a common blind spot — especially when teams assume the provider "handles security."
They don't. You do.
Webhook Provider
│
│ POST /webhook
│ + Signature Header
▼
┌────────────────────┐
│ Middleware / Filter│
│ - Cache body │
│ - Verify HMAC │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Business Logic │
│ - Process event │
└────────────────────┘Signature verification should happen before any business logic — not inside it.
HMAC-SHA256 Verification
Most providers (Stripe, Shopify, GitHub) sign webhook payloads using HMAC-SHA256. Verify the signature before processing the request.
String receivedSignature = request.getHeader("X-Shopify-Hmac-SHA256");
String computedSignature = computeHmac(requestBody, webhookSecret);
if (!constantTimeEquals(receivedSignature, computedSignature)) {
throw new UnauthorizedException("Invalid webhook signature");
}The Real Trap: Signature Comparison
The most common webhook vulnerability isn't missing HMAC verification.
It's verifying the signature incorrectly.
Always use constant-time comparison.
Never use ==, .equals(), or anything that short-circuits on mismatch — those leak timing information.
Request Body Caching
Request bodies can only be read once. Cache the body at the middleware level so you can:
- Verify the signature
- Then process the payload safely
Cache first. Verify second. Process last.
The Bottom Line
Security headers, rate limiting, CORS, and webhook verification are what protect your application from abuse at scale.
They're easy to skip early.
They're painful to add after an incident.
Combined with authentication, validation, and encryption from Part 1, these controls create defense in depth — multiple layers that force attackers to give up or move on.
Every incident I've seen in this category was preventable with one of the controls above — and none of them required advanced attackers.
Security isn't about one perfect solution.
It's about layers that make your system resilient against the attacks that actually happen.
At The Better Software Initiative, we build security into every layer of the applications we develop. From authentication architecture to production hardening, we can help you build systems that are secure by design. Let's talk.

Magdalena Furman
Co-founder & Director @ TBSI
10+ years of experience as a Backend Engineer. Specialized in building secure, scalable systems with Java/Spring Boot, AWS, and Terraform. Expert in application security, authentication architecture, and production-grade system design.