Back to Cheat Sheets

🏗️ Address Security from the Start

OWASP Top 10 Proactive Controls (2024) - C4

CORE CONTROL

📋 What Is This Control?

Address Security from the Start treats security as a design property, not a feature bolted on at the end. Security decisions are woven into every phase of the lifecycle - requirements, architecture, design, implementation, testing, deployment, and operation - so the safe way to build something is also the default way. It is the constructive answer to Insecure Design (A04): a whole class of flaws that perfect coding cannot fix, because the flaw is in the plan itself. The core move is to shift left - catch flaws while they are still a diagram.

C4 Proactive Control
Secure Design Focus Area
OWASP 2024 Proactive Controls

🎯 What It Prevents

  • Client-Side-Only Business Rules: Price/quantity/role trusted from the client
  • Missing Anti-Automation: Credential stuffing, enumeration, hoarding bots
  • Implicit Internal Trust: Flat networks letting an attacker pivot freely
  • Skippable Workflow Steps: Calling the final step without the prior ones
  • Mass Assignment: Binding privilege fields straight from the request body
  • Absent Controls: A missing limit, boundary, or rule no pentest will trip over
  • Single-Point Failure: One bug meaning full compromise (no layering)

🧭 How It Works

1. Draw it - a data-flow diagram with trust boundaries
↓
2. Threat model - walk STRIDE: what can go wrong?
↓
3. Write security requirements & abuse cases as testable items
↓
4. Choose controls from a reusable secure-pattern catalog
↓
5. Validate: abuse cases become tests; re-model on change

❌ Without This Control

# Insecure design: the code runs perfectly - a required control is missing from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/api/transfer', methods=['POST']) def transfer(): data = request.get_json() # DESIGN GAPS: # - 'from_account' comes from the client (no ownership check) # - no rule that amount > 0 or amount <= balance # - no idempotency: a double-submit transfers twice move_money(data['from_account'], data['to_account'], data['amount']) return jsonify({'status': 'ok'}) # Attacker: {"from_account":"any","to_account":"me","amount":-999999} // Insecure design: multi-step flow with no enforced sequence (Node) app.post('/reset/complete', (req, res) => { // DESIGN GAP: nothing proves step 1 happened for THIS user. // No token, no state → attacker calls this directly. setPassword(req.body.email, req.body.newPassword); res.json({ ok: true }); });

✅ Applying the Control

# Secure design: identity from session, server-side invariants, idempotency @app.route('/api/transfer', methods=['POST']) @require_auth # authentication is a boundary requirement def transfer(): data = request.get_json() src = get_account_for_user(g.current_user) # identity derived server-side amount = Decimal(str(data.get('amount', '0'))) idem_key = request.headers.get('Idempotency-Key') if amount <= 0 or amount > MAX_TRANSFER: # invariant: bounded amount return jsonify({'error': 'invalid amount'}), 400 if amount > src.balance: return jsonify({'error': 'insufficient funds'}), 400 if not owns(g.current_user, src): # ownership check return jsonify({'error': 'forbidden'}), 403 if already_processed(idem_key): # idempotent: double-submit safe return jsonify({'status': 'ok', 'replay': True}) with transaction(): # atomic; no partial / race state move_money(src, data['to_account'], amount) record(idem_key) audit_log('transfer', user=g.current_user, amount=amount) # repudiation control return jsonify({'status': 'ok'}) # Explicit DTO stops mass assignment - only allow-listed fields are bindable (Java) record UserUpdateDto(String displayName, String avatarUrl) {} // isAdmin unreachable

✓ Implementation Checklist

  • Security activities attached to every SDLC phase with named owners
  • Every meaningful feature gets a lightweight threat model (DFD + STRIDE)
  • Security requirements and abuse cases are written and become tests
  • A secure-pattern catalog and reference architectures exist and are reused
  • A paved-road platform provides secure defaults and guardrails
  • Defense in depth and least privilege are explicit in the architecture
  • Trust boundaries are drawn; crossings re-authenticate, re-authorize, re-validate
  • Business rules are enforced server-side, safe under concurrency
  • Design review and CI security gates run early and block on high severity
  • Proven frameworks are used for all security primitives
  • Security champions are embedded; threats prioritised by risk; accepted risks documented

🔍 Tools & Techniques

Design & Requirements:

STRIDE Data-Flow Diagrams OWASP Threat Dragon OWASP ASVS OWASP SAMM BSIMM

Shift-Left CI Gates:

Semgrep (SAST) dependency-check gitleaks Checkov (IaC)

How to Verify:

  • Confirm each meaningful feature has a threat model before build
  • Check that abuse cases exist as passing acceptance tests
  • Verify business rules are enforced server-side, not only in the client
  • Trace each trust-boundary crossing for re-auth and re-validation
  • Confirm CI blocks the build on HIGH/CRITICAL findings

🌍 Why It Matters

  • Client-side-only business rules: An attacker bypasses the UI and posts a negative quantity or tampered price. Price-tampering and coupon-stacking abuse are classic examples; re-validating every rule server-side is the design fix.
  • Missing anti-automation on sensitive flows: Credential stuffing and gift-card enumeration exploit rate limiting that was never in the design. Naming the abuse case up front requires the throttle.
  • Implicit trust between internal services: A flat "inside the network" trust model lets one foothold pivot everywhere. Explicit boundaries and service-to-service auth contain it.
  • Skippable workflow steps: Calling the final step directly - fulfilling without paying, resetting without proving identity - is defeated by a server-side state machine.

These are documented classes of design failure, described as patterns rather than tied to any vendor or CVE. Pentests rarely catch a control that simply does not exist - design review and threat modeling do.

📌 Quick Tips

  • DO NOT add security last, after the feature already works
  • DO NOT trust the client to assert identity, price, or role
  • DO NOT assume a passed pentest means the design is sound
  • DO threat model early with a DFD and STRIDE
  • DO write abuse cases as testable requirements
  • DO enforce sequence, limits, and ownership server-side
  • DO make the secure path the default via paved roads and proven frameworks