Skip to content

Chapter 30: Application Security

Overview

Application security (AppSec) addresses vulnerabilities introduced during software design, development, deployment, and maintenance. With 75% of attacks now targeting the application layer (Gartner), organizations must embed security throughout the software development lifecycle rather than applying it as an afterthought. This chapter covers the OWASP Top 10, secure coding principles, threat modeling, SAST/DAST/IAST, API security, secure DevOps (DevSecOps), and web application firewall deployment.

Learning Objectives

By the end of this chapter, students SHALL be able to:

  1. Apply OWASP Top 10 knowledge to code review and threat assessment
  2. Conduct threat modeling using STRIDE for new application designs
  3. Integrate SAST, DAST, and SCA into software development pipelines
  4. Assess and secure RESTful and GraphQL API attack surfaces
  5. Design and operate a Web Application Firewall (WAF) deployment
  6. Build an AppSec program using OWASP SAMM or BSIMM benchmarks

Prerequisites

  • Basic programming knowledge (any language)
  • Understanding of HTTP, REST APIs, and web architecture
  • Chapter 29 (Vulnerability Management) for scanning tools context

Why This Matters

The Log4Shell vulnerability (CVE-2021-44228) was in a logging library used by millions of Java applications — a single insecure code pattern (JNDI lookup in log messages) enabled remote code execution across an estimated 3 billion devices. SQL injection, still #3 in OWASP Top 10 2021, has existed since 1998 and continues to cause major breaches because developers still don't use parameterized queries. Equifax's 2017 breach of 147 million Americans came from an unpatched Apache Struts vulnerability in a consumer-facing app. Application security is both the largest attack surface and the most preventable category of vulnerability.


30.1 OWASP Top 10 (2021)

30.1.1 A01 — Broken Access Control

The most common web application vulnerability (94% of applications tested).

# VULNERABLE: Direct Object Reference
@app.route("/invoice/<invoice_id>")
def get_invoice(invoice_id):
    invoice = db.query("SELECT * FROM invoices WHERE id = ?", invoice_id)
    return jsonify(invoice)  # Anyone can access any invoice by changing the ID!

# SECURE: Enforce ownership check
@app.route("/invoice/<invoice_id>")
@login_required
def get_invoice(invoice_id):
    invoice = db.query(
        "SELECT * FROM invoices WHERE id = ? AND user_id = ?",
        invoice_id, current_user.id  # Ensure user owns this invoice
    )
    if not invoice:
        abort(403)  # Forbidden — not found or not owned
    return jsonify(invoice)

30.1.2 A02 — Cryptographic Failures

# VULNERABLE: Weak hashing, no salt
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()  # Crackable in seconds

# VULNERABLE: Broken encryption
from Crypto.Cipher import DES  # DES — 56-bit key, broken
cipher = DES.new(key, DES.MODE_ECB)  # ECB mode reveals patterns

# SECURE: Modern password hashing
import bcrypt
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

# SECURE: Authenticated encryption
from cryptography.fernet import Fernet
key = Fernet.generate_key()  # Store securely!
f = Fernet(key)
encrypted = f.encrypt(sensitive_data.encode())

# SECURE: For databases — use AES-256-GCM with unique IV per record
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12)  # MUST be unique per encryption operation
ciphertext = aesgcm.encrypt(nonce, plaintext, additional_data)

30.1.3 A03 — Injection

# VULNERABLE: SQL Injection
username = request.args.get('username')
query = f"SELECT * FROM users WHERE username = '{username}'"
# Payload: ' OR '1'='1  → dumps entire table
# Payload: '; DROP TABLE users; --  → destroys data

# SECURE: Parameterized queries (prepared statements)
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))  # Database handles escaping

# SECURE: ORM (SQLAlchemy)
user = session.query(User).filter(User.username == username).first()

# COMMAND INJECTION
# VULNERABLE:
result = os.system(f"ping {user_input}")  # User can inject: '; rm -rf /'

# SECURE: Use subprocess with argument list (no shell=True)
import subprocess
result = subprocess.run(
    ["ping", "-c", "4", user_input],  # Arguments as list — no shell interpolation
    capture_output=True, text=True, timeout=30
)

30.1.4 A04 — Insecure Design

Threat modeling identifies insecure design before code is written. STRIDE is the most widely used methodology:

Threat Description Example
Spoofing Impersonating another user/system Weak session tokens
Tampering Modifying data in transit or at rest No message integrity checks
Repudiation Denying an action was taken Insufficient logging
Information Disclosure Exposing confidential data Verbose error messages
Denial of Service Making service unavailable No rate limiting
Elevation of Privilege Gaining unauthorized access IDOR, privilege escalation

30.1.5 A05 — Security Misconfiguration

# VULNERABLE: Django default settings
DEBUG = True  # Never in production — exposes stack traces
SECRET_KEY = "insecure-default-key"
ALLOWED_HOSTS = ["*"]  # Allows all hosts

# SECURE: Production Django settings
DEBUG = False
SECRET_KEY = os.environ.get("SECRET_KEY")  # From secrets manager
ALLOWED_HOSTS = ["app.example.com"]
SECURE_HSTS_SECONDS = 31536000  # HSTS for 1 year
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = "DENY"
SECURE_CONTENT_TYPE_NOSNIFF = True

30.1.6 A07 — Identification and Authentication Failures

# SECURE: Authentication implementation checklist
class SecureAuth:

    def login(self, username: str, password: str) -> bool:
        # 1. Use timing-safe comparison (prevent timing attacks)
        user = self.get_user(username)
        if not user:
            # Compare anyway — prevent username enumeration via timing
            bcrypt.checkpw(password.encode(), b"$2b$12$dummy_hash_for_timing")
            return False

        # 2. Use bcrypt with cost factor ≥ 10
        if not bcrypt.checkpw(password.encode(), user.password_hash):
            self.log_failed_login(username, request.remote_addr)
            self.increment_lockout_counter(username)
            return False

        # 3. Account lockout (5 attempts)
        if user.failed_logins >= 5:
            raise AccountLockedException("Account locked. Use password reset.")

        # 4. MFA required for all accounts
        if not self.verify_mfa(user, request.form.get("totp_code")):
            return False

        # 5. Regenerate session on login (prevent session fixation)
        session.regenerate()
        session["user_id"] = user.id
        return True

30.1.7 A08 — Software and Data Integrity Failures

See Chapter 24 (Supply Chain Attacks) for full treatment of dependency integrity and CI/CD security.

# Verify package integrity before use
pip install --require-hashes -r requirements.txt

# requirements.txt with hashes:
requests==2.31.0 \
    --hash=sha256:58cd2187423839d5cec5c8c50a66b0571b4f19ca238e75399b680c58cffb84a7

30.1.8 A10 — Server-Side Request Forgery (SSRF)

# VULNERABLE: Fetch user-provided URL
import requests
url = request.args.get("url")
response = requests.get(url)  # Attacker can access: http://169.254.169.254/latest/meta-data/

# SECURE: Validate and allowlist URLs
from urllib.parse import urlparse
import ipaddress

ALLOWED_SCHEMES = {"https"}
ALLOWED_DOMAINS = {"api.partner.com", "cdn.example.com"}

def safe_fetch(url: str) -> requests.Response:
    parsed = urlparse(url)

    if parsed.scheme not in ALLOWED_SCHEMES:
        raise ValueError(f"Disallowed scheme: {parsed.scheme}")

    # Resolve hostname to IP and check for private ranges
    import socket
    ip = socket.gethostbyname(parsed.netloc)
    addr = ipaddress.ip_address(ip)
    if addr.is_private or addr.is_loopback or addr.is_link_local:
        raise ValueError(f"Internal IP not allowed: {ip}")

    if parsed.netloc not in ALLOWED_DOMAINS:
        raise ValueError(f"Domain not in allowlist: {parsed.netloc}")

    return requests.get(url, timeout=5)

30.2 API Security

30.2.1 OWASP API Security Top 10

Rank Vulnerability Example
API1 Broken Object Level Authorization Access other users' resources by changing ID
API2 Broken Authentication Weak JWT secrets, no token expiry
API3 Broken Object Property Level Auth Mass assignment vulnerability
API4 Unrestricted Resource Consumption No rate limiting → DoS
API5 Broken Function Level Authorization Non-admin accessing admin endpoints
API6 Unrestricted Access to Sensitive Business Flows Account takeover via API chain
API7 Server Side Request Forgery Webhook URL points to internal services
API8 Security Misconfiguration CORS misconfiguration, verbose errors
API9 Improper Inventory Management Shadow APIs, undocumented endpoints
API10 Unsafe Consumption of APIs Trusting 3rd-party API responses without validation

30.2.2 JWT Security

# VULNERABLE: JWT verification
import jwt
token = request.headers["Authorization"].split(" ")[1]
payload = jwt.decode(token, options={"verify_signature": False})  # NEVER DO THIS

# VULNERABLE: Algorithm confusion attack
# Attacker changes alg to "none" — accept token without signature
payload = jwt.decode(token, algorithms=["HS256", "none"])  # Don't allow "none"!

# SECURE: Strict JWT validation
import jwt
from functools import wraps

JWT_SECRET = os.environ["JWT_SECRET"]  # 256-bit random from secrets manager
JWT_ALGORITHM = "RS256"  # Prefer asymmetric (RS256/ES256) over HS256

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get("Authorization")
        if not auth_header or not auth_header.startswith("Bearer "):
            return jsonify({"error": "Unauthorized"}), 401
        token = auth_header.split(" ")[1]
        try:
            payload = jwt.decode(
                token,
                JWT_PUBLIC_KEY,
                algorithms=["RS256"],  # Strict algorithm list — no "none", no HS256 if using RS256
                options={
                    "verify_exp": True,  # Verify expiry
                    "verify_nbf": True,  # Verify not-before
                    "require": ["exp", "iat", "sub"]  # Required claims
                }
            )
        except jwt.ExpiredSignatureError:
            return jsonify({"error": "Token expired"}), 401
        except jwt.InvalidTokenError as e:
            return jsonify({"error": "Invalid token"}), 401
        g.user_id = payload["sub"]
        return f(*args, **kwargs)
    return decorated

30.3 Secure Development Lifecycle

30.3.1 Microsoft SDL Activities by Phase

flowchart LR
    A[Requirements] --> B[Design] --> C[Implementation] --> D[Verification] --> E[Release] --> F[Response]

    A --> A1[Security requirements\nAbuse cases]
    B --> B1[Threat modeling\nSTRIDE/PASTA\nSecurity architecture review]
    C --> C1[Secure coding standards\nSAST in IDE\nPeer code review]
    D --> D1[SAST + DAST\nPentest\nSCA dependency scan]
    E --> E1[Penetration test\nFinal security review\nIncident response plan]
    F --> F1[Bug bounty\nVulnerability disclosure\nPatch management]

30.3.2 OWASP SAMM — Software Assurance Maturity Model

SAMM provides a measurable framework for assessing and improving AppSec programs across 15 security practices in 5 business functions:

OWASP SAMM v2 Structure:
├── Governance
│   ├── Strategy and Metrics
│   ├── Policy and Compliance
│   └── Education and Guidance
├── Design
│   ├── Threat Assessment
│   ├── Security Requirements
│   └── Security Architecture
├── Implementation
│   ├── Secure Build
│   ├── Secure Deployment
│   └── Defect Management
├── Verification
│   ├── Architecture Assessment
│   ├── Requirements-Driven Testing
│   └── Security Testing
└── Operations
    ├── Incident Management
    ├── Environment Management
    └── Operational Management

Each practice measured at Level 1, 2, or 3.
Target: Defined by org risk appetite and regulatory requirements.

30.4 Web Application Firewall

30.4.1 WAF Deployment Modes

graph LR
    subgraph "Client"
        USER[User Browser]
    end

    subgraph "WAF Layer"
        WAF[Web Application Firewall\nModSecurity / Cloudflare / AWS WAF]
    end

    subgraph "Application"
        APP[Web Application\nAPI / Service]
    end

    USER --> WAF --> APP

    WAF -.->|Block| BL[Blocked Requests:\n- SQLi patterns\n- XSS patterns\n- Path traversal\n- Scanner signatures\n- Bot traffic]

30.4.2 ModSecurity / OWASP CRS

# Nginx + ModSecurity 3 + OWASP CRS configuration

# /etc/nginx/nginx.conf
load_module modules/ngx_http_modsecurity_module.so;

server {
    listen 443 ssl;
    server_name app.example.com;

    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req zone=api burst=20 nodelay;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-{RANDOM}'; style-src 'self' 'unsafe-inline'";
    add_header Referrer-Policy "strict-origin-when-cross-origin";
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
}

30.5 Security Headers

# REQUIRED Security Response Headers

# HSTS — force HTTPS for 1 year
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# Prevent clickjacking
X-Frame-Options: DENY

# Prevent MIME sniffing
X-Content-Type-Options: nosniff

# CSP — restrict resource loading
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'self'

# Referrer policy
Referrer-Policy: strict-origin-when-cross-origin

# Permissions policy — disable browser features
Permissions-Policy: geolocation=(), microphone=(), payment=()

# Remove server information
Server: (remove this header entirely)
X-Powered-By: (remove this header entirely)

30.6 Benchmark Controls

Control ID Title Requirement
Nexus SecOps-AS-01 Secure SDLC Documented secure development lifecycle; threat modeling for new features
Nexus SecOps-AS-02 SAST/DAST in CI/CD Automated SAST and DAST in pipeline; critical findings block deployment
Nexus SecOps-AS-03 Penetration Testing Annual pentest for all externally-facing applications
Nexus SecOps-AS-04 WAF Deployment WAF deployed in front of all internet-facing applications
Nexus SecOps-AS-05 Security Headers All OWASP recommended security headers deployed
Nexus SecOps-AS-06 API Security Authentication, rate limiting, and input validation on all APIs

Exam Prep & Certifications

Relevant Certifications

The topics in this chapter align with the following certifications:

  • CSSLP — Domains: Secure Software Lifecycle, Secure Design, Security Testing
  • GIAC GWEB — Domains: Web Application Security, Secure Development
  • GIAC GWAPT — Domains: Web Application Penetration Testing, OWASP Top 10

View full Certifications Roadmap →

Key Terms

CSP (Content Security Policy) — An HTTP response header that instructs browsers to only load resources from specified, trusted sources — preventing XSS and data injection.

DAST (Dynamic Application Security Testing) — Testing a running application from the outside by sending inputs and observing responses, simulating attacker behavior.

OWASP — Open Web Application Security Project — a non-profit foundation producing free, open-source methodologies, tools, and documentation for application security.

SAST (Static Application Security Testing) — Analyzing source code or compiled binaries without execution to identify security vulnerabilities.

STRIDE — Microsoft's threat modeling framework categorizing threats as: Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege.

WAF (Web Application Firewall) — A security control that monitors, filters, and blocks HTTP/HTTPS traffic to and from a web application, protecting against common attacks.