OWASP #

Most security breaches in web applications aren’t caused by sophisticated, never-before-imagined attacks. The majority come from the same categories, repeating year after year, with known prevention methods. OWASP — the Open Web Application Security Project — documents these categories in a Top 10 list updated periodically based on real incident data from hundreds of organizations worldwide.

Understanding the OWASP Top 10 isn’t about memorizing names. It’s about understanding how attackers think — how they identify and exploit weaknesses that look harmless from the perspective of a developer only thinking about the happy path. Every item in this list represents one way developer assumptions about security turn out not to hold in the real world.

What Is OWASP and Why It Matters #

OWASP is a nonprofit organization producing security documentation, tools, and standards — all available for free. The most well-known is the OWASP Top 10 — a list of the ten most critical vulnerability categories updated every few years based on real incident data.

The current OWASP Top 10 (2021) reflects a shift in the threat landscape: from purely technical attacks toward a combination of technical and design issues. Two new items — Insecure Design and Software Integrity Failures — show that security can’t just be added at the end as a layer; it must be part of how systems are designed from the start.

graph TD
    A[Attacker] --> B{Entry Points}
    B --> C[User Input]
    B --> D[API Endpoints]
    B --> E[Authentication]
    B --> F[Third-party Components]

    C --> G["Injection / XSS"]
    D --> H["Broken Access Control / SSRF"]
    E --> I[Auth Failures]
    F --> J["Vulnerable Components / Supply Chain"]

    G --> K["Data Breach / RCE"]
    H --> K
    I --> L[Account Takeover]
    J --> K

    K --> M[Reputation, Financial, Legal]
    L --> M

A01 — Broken Access Control #

Broken Access Control has been the number one risk since 2021, up from position five in the previous list. It happens when systems don’t properly verify whether the user making a request is authorized to perform that operation.

The key to understanding this risk: authorization and authentication are two different things. Authentication verifies who you are. Authorization verifies what you’re allowed to do. Many systems have strong authentication but weak authorization.

// ANTI-PATTERN: no authorization at the endpoint — only checks login
func getOrder(w http.ResponseWriter, r *http.Request) {
    orderID := r.PathValue("order_id")
    order := findOrder(orderID)
    writeJSON(w, http.StatusOK, order)
    // A logged-in attacker can access other users' orders
    // just by changing order_id in the URL:
    // GET /orders/1001 → GET /orders/1002 → GET /orders/1003
    // This is called Insecure Direct Object Reference (IDOR)
}

// CORRECT: verify ownership before granting access
func getOrder(w http.ResponseWriter, r *http.Request) {
    orderID := r.PathValue("order_id")
    order, err := findOrderOr404(orderID)
    if err != nil {
        http.NotFound(w, r)
        return
    }

    // Verify this order belongs to the currently logged-in user
    if order.UserID != currentUser(r).ID {
        http.Error(w, "Forbidden", http.StatusForbidden) // Forbidden — not yours
        return
    }

    writeJSON(w, http.StatusOK, order)
}
Broken Access Control mitigation principles:

  ✓ Verify ownership on every request accessing a specific resource
  ✓ Implement strict RBAC (Role-Based Access Control)
  ✓ Deny by default — if no explicit rule allows it, reject it
  ✓ Never rely on client-side for any access control
  ✓ Log all access control failures — repeated failure patterns = IDOR attacks
  ✗ Don't expose raw database primary keys in URLs (use UUIDs or tokens)

A02 — Cryptographic Failures #

Previously named “Sensitive Data Exposure”, the newer name shifts focus to the root cause: failures in using cryptography correctly are what cause sensitive data to be exposed.

This isn’t just about not encrypting data — it’s about using the wrong encryption, outdated algorithms, or implementations that look correct but have fundamental weaknesses.

// ANTI-PATTERN: storing passwords in plaintext
func registerUser(email, password string) {
    user := User{Email: email, Password: password} // ← VERY DANGEROUS
    db.Add(&user)
}

// ANTI-PATTERN: using MD5 or SHA1 for passwords
func registerUser(email, password string) {
    hashed := md5.Sum([]byte(password)) // ← MD5 is unsafe for passwords
    user := User{Email: email, Password: fmt.Sprintf("%x", hashed)}
    db.Add(&user)
}

// CORRECT: use bcrypt or Argon2 with auto-generated salts
// golang.org/x/crypto/argon2 — the salt must be random and stored with the hash
func registerUser(email, password string) {
    salt := make([]byte, 16)
    rand.Read(salt)
    hashed := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
    user := User{Email: email, PasswordHash: fmt.Sprintf("%x:%x", salt, hashed)}
    db.Add(&user)
}

func verifyPassword(plainPassword, storedHash string) bool {
    // Argon2 is designed for slow hashing — making brute force very expensive
    parts := strings.Split(storedHash, ":")
    salt, _ := hex.DecodeString(parts[0])
    expected, _ := hex.DecodeString(parts[1])
    actual := argon2.IDKey([]byte(plainPassword), salt, 1, 64*1024, 4, 32)
    return subtle.ConstantTimeCompare(actual, expected) == 1
}
What makes password hashing correct:

  ✓ Use algorithms designed for passwords: Argon2, bcrypt, scrypt
    Not: MD5, SHA1, SHA256 without additional parameters
    Reason: general-purpose hash algorithms are designed for speed. Password
    hashing needs slowness — one hash taking 100ms is a feature, not a bug.

  ✓ Salts generated randomly for every password (bcrypt/Argon2 do this automatically)
    Without salts: two users with the same password have the same hash
    → rainbow table attacks become effective

  ✓ Encrypt sensitive data at rest
    Compromised database → sensitive data stays unreadable

  ✓ TLS for all communication (HTTP → HTTPS)
    Network traffic can't be sniffed

  ✗ Don't hardcode encryption keys in source code
  ✗ Don't use ECB mode for block encryption
  ✗ Don't generate random numbers with Math.random() for security purposes

A03 — Injection #

Injection happens when user-supplied data is executed as commands or queries. SQL Injection is the most common, but this category includes Command Injection, LDAP Injection, and various other forms.

What makes injection dangerous: attackers don’t need accounts, valid sessions, or complex bug discovery. One vulnerable endpoint is enough to expose an entire database.

// ANTI-PATTERN: string concatenation directly into an SQL query
func getUserByEmail(email string) {
    query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
    return db.Execute(query)
}

// What happens if the attacker sends an email:
// email = "anything' OR '1'='1"
// Query becomes: SELECT * FROM users WHERE email = 'anything' OR '1'='1'
// → returns ALL users in the database

// Even more dangerous — DROP TABLE:
// email = "'; DROP TABLE users; --"
// Query becomes: SELECT * FROM users WHERE email = ''; DROP TABLE users; --'
// → deletes the entire users table

// CORRECT: parameterized queries — input is always treated as data, not code
func getUserByEmail(email string) {
    query := "SELECT * FROM users WHERE email = ?"
    return db.Execute(query, email)
    // The database engine separates query structure from data
    // Input ''; DROP TABLE users; --' is searched literally,
    // not executed as SQL
}

// CORRECT with an ORM (more idiomatic):
func getUserByEmail(email string) {
    return db.Where("email = ?", email).First(&User{})
    // Good ORMs always use parameterized queries underneath
}
// Command Injection — equally dangerous

// ANTI-PATTERN: user input directly into a shell command
func convertImage(filename string) {
    exec.Command("sh", "-c", "convert "+filename+" output.png").Run()
    // If filename = "test.jpg; rm -rf /", the server runs rm -rf /
}

// CORRECT: use list arguments, not a shell
func convertImage(filename string) error {
    // Validate the filename first
    for _, c := range filename {
        if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '.' && c != '_' {
            return errors.New("invalid filename")
        }
    }
    return exec.Command("convert", filename, "output.png").Run()
    // Each argument is treated as one unit, not parsed by the shell
}

A04 — Insecure Design #

This is a new category in 2021 answering an important question: what if the implementation is correct, but the design is wrong? Insecure Design happens when systems are designed without considering how attackers will try to exploit them.

Examples of Insecure Design in a password reset flow:

  Insecure design:
  1. User requests a password reset
  2. The server sends a link: /reset?token=12345
     (the token is a sequential number, easy to guess)
  3. The attacker tries: /reset?token=12344, 12346, 12347...
  4. The attacker can reset other people's passwords

  Secure design:
  1. User requests a password reset
  2. The server generates a cryptographically secure random token (256-bit)
  3. The token is hashed before being stored in the database
  4. The link is sent: /reset?token=<random-256-bit>
  5. The token is only valid for 15 minutes and single-use
  6. After use, the token is immediately deleted from the database
// Secure password reset token implementation
func createPasswordResetToken(userID int64) (string, error) {
    // Generate a cryptographically secure token (32 bytes = 256-bit entropy)
    rawToken, err := randomURLSafe(32)
    if err != nil {
        return "", err
    }

    // Hash the token before storing — if the database leaks, tokens can't be used
    tokenHash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawToken)))

    // Store the hash and expiry time
    PasswordResetToken.Create(PasswordResetToken{
        UserID:    userID,
        TokenHash: tokenHash,
        ExpiresAt: time.Now().Add(15 * time.Minute),
        Used:      false,
    })

    // Return the raw token to send to the user (not the hash)
    return rawToken, nil
}

func verifyResetToken(rawToken, newPassword string) error {
    tokenHash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawToken)))

    record, err := PasswordResetToken.FindByHash(tokenHash)
    if err != nil {
        return errors.New("invalid token")
    }
    if record.Used {
        return errors.New("token already used")
    }
    if record.ExpiresAt.Before(time.Now()) {
        return errors.New("token has expired")
    }

    // Update the password and invalidate the token
    updateUserPassword(record.UserID, newPassword)
    record.MarkAsUsed() // The token can only be used once
    return nil
}
Threat modeling is the systematic way to identify Insecure Design before implementation begins. The main question: “What could an attacker do with this feature?” Do threat modeling for every feature touching authentication, authorization, sensitive data, or financial flows.

A05 — Security Misconfiguration #

Misconfiguration is one of the most frequent and most easily avoidable causes of security incidents. Servers displaying stack traces, debug endpoints open in production, unchanged default credentials, public cloud storage buckets — these aren’t bugs in code, but in configuration.

Common Security Misconfiguration examples:

  ✗ Error handling exposing sensitive information:
    HTTP 500: java.sql.SQLException: Table 'users' doesn't exist
    → Attackers learn the database table name and that they can try
      SQL injection to find existing tables

  ✓ Safe error handling:
    HTTP 500: Internal Server Error
    → No internal information exposed
    → Error details logged server-side only

  ✗ HTTP headers exposing technology:
    X-Powered-By: Express 4.18.2
    Server: Apache/2.4.51 (Ubuntu)
    → Attackers know the versions used and can search for relevant CVEs

  ✓ Remove technology-exposing headers:
    # Express.js
    app.disable('x-powered-by')
    # Nginx
    server_tokens off;

  ✗ Overly permissive CORS:
    Access-Control-Allow-Origin: *
    → Any website can make requests to your API on behalf of users

  ✓ Strict CORS:
    Access-Control-Allow-Origin: https://app.yourdomain.com
# Security headers that must exist on every HTTP response:

# Content Security Policy — prevents XSS
Content-Security-Policy: default-src 'self'; script-src 'self'

# Prevents MIME type sniffing
X-Content-Type-Options: nosniff

# Prevents clickjacking
X-Frame-Options: DENY

# Forces HTTPS for 1 year
Strict-Transport-Security: max-age=31536000; includeSubDomains

# Don't send Referer to other sites
Referrer-Policy: strict-origin-when-cross-origin

A06 — Vulnerable and Outdated Components #

Modern applications aren’t written from scratch — they depend on hundreds of third-party libraries and frameworks. Every dependency is a potential attack vector. When a vulnerability is found in a library you use, attackers exploiting your app don’t need to find new bugs — they just use already-available exploits.

The vulnerability lifecycle in dependencies:

  1. A researcher finds a vulnerability in library X version 1.2.3
  2. A CVE is published: "Library X before 1.2.4 is vulnerable to RCE"
  3. A patch is released: library X version 1.2.4
  4. Exploit tools are published on GitHub/ExploitDB
  5. Automated scanners (Shodan, etc.) find apps still using 1.2.3
  6. Mass exploitation begins — unupdated apps become targets

  As long as you use the old version, you're exposed.
  How long between step 2 and step 6? Could be hours.
# Integrate dependency scanning into the CI pipeline
# GitHub Actions example:

name: Security Scan

on: [push, pull_request]

jobs:
  dependency-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # For Node.js
      - name: NPM Audit
        run: npm audit --audit-level=high
        # Fail the build if there are high-severity vulnerabilities

      # For Python
      - name: Safety Check
        run: |
          pip install safety
          safety check --full-report          

      # For Go
      - name: GoVulnCheck
        run: |
          go install golang.org/x/vuln/cmd/govulncheck@latest
          govulncheck ./...          

      # OWASP Dependency-Check (universal)
      - name: OWASP Dependency Check
        uses: dependency-check/Dependency-Check_Action@main
        with:
          project: 'my-app'
          path: '.'
          format: 'HTML'
          args: >
            --failOnCVSS 7
            --enableRetired            

A07 — Identification and Authentication Failures #

Weak authentication is the most direct gateway into user accounts. This isn’t only about guessable passwords — it’s about all the mechanisms ensuring the person claiming to be user X really is user X.

// ANTI-PATTERN: no rate limiting on the login endpoint
func login(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    json.NewDecoder(r.Body).Decode(&req)

    user := findUserByEmail(req.Email)
    if user != nil && checkPassword(user, req.Password) {
        writeJSON(w, http.StatusOK, map[string]string{"token": generateToken(user)})
        return
    }
    writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
    // Problem: attackers can try thousands of password combinations per minute
    // Brute force and credential stuffing go undetected
}

// CORRECT: rate limiting, account lockout, and logging
func login(w http.ResponseWriter, r *http.Request) {
    // Rate limit: max 5 attempts per minute per IP
    if !rateLimiter.Allow(r.RemoteAddr) {
        http.Error(w, "Too many attempts", http.StatusTooManyRequests)
        return
    }

    var req struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    json.NewDecoder(r.Body).Decode(&req)

    user := findUserByEmail(req.Email)

    // Constant-time comparison to prevent timing attacks
    // Don't early-return if the user isn't found
    if user == nil {
        // Still run a dummy hash verification for consistent timing
        argon2id.Verify("$argon2id$...", req.Password) // will fail but takes the same time
        writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
        return
    }

    if !argon2id.Verify(user.PasswordHash, req.Password) {
        // Log the failed attempt for monitoring
        logFailedLogin(req.Email, r.RemoteAddr)
        writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
        return
    }

    writeJSON(w, http.StatusOK, map[string]string{"token": generateToken(user)})
}
Authentication mechanisms that should exist:

  ✓ Multi-Factor Authentication (MFA) for sensitive accounts
    → Even if passwords are compromised, attackers need a second factor

  ✓ Rate limiting and account lockout
    → After N failed attempts, the account locks temporarily

  ✓ Credential stuffing detection
    → Logins from unusual locations → trigger additional verification

  ✓ Proper session management
    → Session tokens invalidated on logout
    → Sessions expired after inactivity

  ✗ Don't allow passwords like "password", "123456", or passwords on
    common password lists (the HIBP API can be used to check)
  ✗ Don't expose "email not registered" vs "wrong password" information
    → Use the same message: "Invalid email or password"

A08 — Software and Data Integrity Failures #

This category covers two related problems: blind trust in external software (supply chain attacks) and the lack of integrity verification on updates or critical data.

Real-world supply chain attack examples:

  2020 — SolarWinds:
  Attackers infiltrated the SolarWinds build pipeline
  → Infected software updates distributed to 18,000+ customers
  → Including US government agencies

  2021 — Codecov:
  The Codecov upload script was compromised
  → All CI pipelines using Codecov leaked environment variables
  → Including AWS credentials, GitHub tokens, etc.

  Lesson: you don't just need to trust the code you write,
  but also all the code you run.
# Supply chain attack mitigations:

# 1. Pin dependencies to exact versions (not ^x.x.x or ~x.x.x)
# package.json:
{
  "dependencies": {
    "express": "4.18.2",      # ✓ exact version
    "lodash": "^4.17.21"      # ✗ could auto-update to the latest 4.x.x
  }
}

# 2. Verify integrity with lock files
npm ci  # use package-lock.json for reproducible installs
        # (not npm install which can update the lock file)

# 3. Subresource Integrity for CDN resources
<script src="https://cdn.example.com/jquery.min.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous">
</script>
# Browsers verify the hash before running the script

A09 — Security Logging and Monitoring Failures #

Systems that aren’t monitored are systems that can’t be defended. Undetected attacks can run for months before discovery — and the longer they run, the bigger the damage.

What must be logged for security purposes:

  Authentication:
  ✓ All login attempts (successful and failed)
  ✓ Password reset requests
  ✓ MFA events (setup, verified, failed)
  ✓ Session creation and termination

  Authorization:
  ✓ Access denied events (403, 401)
  ✓ Privilege escalation attempts
  ✓ Access to sensitive resources

  Data:
  ✓ Large-scale data exports
  ✓ Changes to critical data (passwords, emails, roles)
  ✓ Bulk deletes or updates

  Infrastructure:
  ✓ Detected dependency vulnerabilities
  ✓ Configuration changes
  ✓ New deployments

  Useful log format for security analysis:
  {
    "timestamp": "2025-06-01T14:23:45Z",
    "level": "WARN",
    "event": "login_failed",
    "user_email": "[email protected]",   ← identifies who
    "ip_address": "203.x.x.x",     ← from where
    "user_agent": "...",
    "attempt_count": 5,             ← context
    "request_id": "req_abc123"      ← correlation ID
  }
What must NOT be logged:

  ✗ Passwords (plaintext or hashes)
  ✗ Session tokens or API keys
  ✗ Credit card numbers (even partially masked ones need care)
  ✗ Regulated medical or other sensitive data
  ✗ Password reset tokens

A10 — Server-Side Request Forgery (SSRF) #

SSRF happens when a server makes HTTP requests to user-specified URLs without validating that the URL is safe. Attackers use this to make the server request internal resources that shouldn’t be accessible from outside.

// ANTI-PATTERN: the server fetches user-supplied URLs without validation
func fetchURLPreview(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")
    // The server is now a proxy the attacker can control
    resp, _ := http.Get(url)
    io.Copy(w, resp.Body)
}

// Attackers can send:
// /fetch-preview?url=http://169.254.169.254/latest/meta-data/
// → Access AWS EC2 instance metadata (AWS credentials!)

// /fetch-preview?url=http://localhost:6379
// → Try connecting to a possibly unprotected Redis

// /fetch-preview?url=http://internal-api.company.internal/admin
// → Access internal services not exposed to the internet

// CORRECT: validate and whitelist allowed URLs
var allowedDomains = map[string]bool{
    "api.partner.com":          true,
    "cdn.external-service.com": true,
}

func isSafeURL(rawURL string) bool {
    parsed, err := url.Parse(rawURL)
    if err != nil {
        return false
    }

    // Only allow HTTPS
    if parsed.Scheme != "https" {
        return false
    }

    // Check the domain against the whitelist
    if !allowedDomains[parsed.Hostname()] {
        return false
    }

    // Make sure there's no internal IP address (RFC 1918)
    ip := net.ParseIP(parsed.Hostname())
    if ip != nil && (ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocal()) {
        return false
    }

    return true
}

func fetchURLPreview(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")

    if url == "" || !isSafeURL(url) {
        http.Error(w, "URL not allowed", http.StatusBadRequest)
        return
    }

    client := &http.Client{Timeout: 5 * time.Second} // strict timeout
    resp, _ := client.Get(url)
    io.Copy(w, resp.Body)
}

OWASP ASVS: The Application Security Verification Standard #

The OWASP ASVS (Application Security Verification Standard) is a framework for assessing how secure an application is. Unlike the Top 10, which focuses on risks to avoid, ASVS provides a positive checklist of what should exist.

The three ASVS levels and when to use them:

  Level 1 — Minimum Security (for all applications):
  → Basic protection against the OWASP Top 10
  → Suitable for: low-risk public applications, landing pages
  → Verification: automated security scanning

  Level 2 — Standard Security (for business applications):
  → Defense in depth, proper auth, session management
  → Suitable for: SaaS apps, e-commerce, enterprise applications
  → Verification: manual review + automated testing

  Level 3 — Advanced Security (for critical applications):
  → Formal threat modeling, audited cryptographic implementations
  → Suitable for: banking, healthcare, critical infrastructure
  → Verification: penetration testing + code audits + formal review

Anti-Patterns to Avoid #

✗ "Security later, after the features are done"
  Security added afterwards is almost always incomplete.
  ✓ Security by design: consider threats from the design stage.

✗ Relying on client-side validation alone
  JavaScript can be modified or bypassed entirely.
  ✓ Always validate server-side — client-side is only for UX.

✗ "We don't have sensitive data, no need to worry about security"
  Every app has data valuable to attackers:
  session tokens, user emails, or server capacity to perform
  actions on users' behalf.
  ✓ All applications need basic security.

✗ Security scanning done only once at launch
  New vulnerabilities are found every day in used libraries.
  ✓ Integrate security scanning into the CI pipeline.

✗ Logs that are never read
  Logs that aren't monitored are the same as no logs.
  ✓ Set up alerting for critical security events.

OWASP Checklist for Developers #

AUTHENTICATION & AUTHORIZATION:
  □ All resource-accessing endpoints verify ownership
  □ RBAC implemented and tested
  □ Login endpoints have rate limiting
  □ MFA available for sensitive accounts
  □ Authentication error messages don't distinguish "email not found" vs "wrong password"

CRYPTOGRAPHY:
  □ Passwords hashed with bcrypt/Argon2/scrypt
  □ No sensitive data in plaintext in the database
  □ TLS used for all communication
  □ No hardcoded secrets in source code

INPUT HANDLING:
  □ All database queries use parameterized queries
  □ File uploads validated for type and size
  □ Output encoded before rendering in HTML (XSS prevention)

CONFIGURATION:
  □ Stack traces not shown in production
  □ Security headers installed (CSP, HSTS, X-Frame-Options, etc.)
  □ Default credentials changed
  □ Debug endpoints disabled in production

DEPENDENCY:
  □ Dependency scanning runs in CI
  □ No dependencies with known critical vulnerabilities
  □ Lock files committed for reproducible builds

LOGGING:
  □ Authentication events logged
  □ Authorization failures logged
  □ No credentials or tokens logged
  □ Alerting for security anomalies in place

Summary #

  • The OWASP Top 10 represents how attackers think — understanding it isn’t about memorizing names, but about recognizing weakness patterns that keep recurring across different applications.
  • Broken Access Control is risk #1 — always verify ownership server-side, not just whether the user is logged in. IDOR is a very simple and very common attack.
  • Parameterized queries are the absolute solution for Injection — there’s no valid reason to build SQL queries with string concatenation from user input.
  • Passwords must be hashed with algorithms designed for it — Argon2, bcrypt, or scrypt. MD5, SHA1, and plain SHA256 aren’t for password hashing.
  • Security Misconfiguration is the easiest to avoid — remove debug endpoints, hide stack traces, install security headers, rotate default credentials.
  • Your dependencies are your attack surface — integrate dependency scanning into CI and act fast when CVEs appear in used libraries.
  • MFA and rate limiting are the most effective authentication protections — even strong passwords can be compromised; MFA adds a layer far harder to break.
  • SSRF appears when servers trust user-supplied URLs — whitelist allowed domains, validate no internal IPs, use strict timeouts.
  • Logs that aren’t monitored are useless logs — set up alerting for security events, not just storing logs in unread files.
  • Security is a process, not a destination — new vulnerabilities are found every day. Integrate scanning into CI, patch dependencies routinely, and do periodic security reviews.
#

← Previous: Asynchronous Content Loading   Next: OWASP Cheatsheet

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact