Skip to content

Security Practices That Actually Protect Production Apps (After They've Been Attacked)

Part 2 of a practical production security series

Magdalena Furman
February 3, 2026
4 min read

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/html

After (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-store

This 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 in

Rate 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: true

This 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: true

If 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

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.