The 10 most critical web application security risks and how to mitigate them.
The OWASP Top 10 is the most widely recognized awareness document for web application security. The 2025 edition reflects the latest threat landscape, introducing new categories such as Software Supply Chain Failures and Mishandling of Exceptional Conditions.
Access control enforces policy such that users cannot act outside of their intended permissions. Failures typically lead to unauthorized information disclosure, modification, or destruction of data, or performing a business function outside the user's limits.
Attackers can exploit access control flaws to access other users' accounts, view sensitive files, modify other users' data, or change access rights.
# User ID taken directly from request without authorization check @app.route('/api/user/<user_id>/profile') def get_profile(user_id): user = db.get_user(user_id) # No ownership check! return jsonify(user.to_dict())
@app.route('/api/user/<user_id>/profile') @login_required def get_profile(user_id): # Verify the requesting user owns this resource if current_user.id != user_id and not current_user.is_admin: abort(403) user = db.get_user(user_id) if not user: abort(404) return jsonify(user.to_dict())
Security misconfiguration occurs when security settings are defined, implemented, or maintained incorrectly. This includes missing security hardening, unnecessary features enabled, default accounts with unchanged passwords, overly verbose error messages, and misconfigured HTTP security headers.
Misconfigured servers, frameworks, or cloud services can expose sensitive data, enable unauthorized access, or provide attackers with information to plan further attacks.
# Debug mode enabled in production, verbose errors exposed app = Flask(__name__) app.config['DEBUG'] = True # Exposes stack traces! app.config['SECRET_KEY'] = 'default-secret' # Default key!
import os app = Flask(__name__) app.config['DEBUG'] = False app.config['SECRET_KEY'] = os.environ['SECRET_KEY'] @app.after_request def set_security_headers(response): response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = "default-src 'self'" return response
New in 2025. Focuses on risks related to third-party components, dependencies, and CI/CD pipelines. Attackers target the software supply chain by compromising packages, injecting malicious code into open-source libraries, or exploiting build pipeline vulnerabilities.
A single compromised dependency can affect thousands of applications. Supply chain attacks can lead to data theft, backdoor installation, or complete system compromise with minimal detection.
// package.json with unpinned dependencies { "dependencies": { "express": "*", // Any version! "lodash": "^4.0.0", // Wide range "unknown-pkg": "^1.0.0" // Unvetted package } }
// package.json with pinned versions + lockfile + audit { "dependencies": { "express": "4.21.2", "lodash": "4.17.21" }, "scripts": { "preinstall": "npx npm-audit-resolver", "integrity-check": "npm audit signatures" } } // Also: use package-lock.json, enable Dependabot/Renovate, // generate SBOM, verify package provenance
Failures related to cryptography that often lead to exposure of sensitive data. This includes using deprecated algorithms, weak key generation, missing encryption for data in transit or at rest, and improper certificate validation.
Weak or missing encryption can expose passwords, credit card numbers, health records, personal information, and business secrets, potentially leading to regulatory violations (GDPR, PCI DSS).
import hashlib # Storing passwords with weak hashing def store_password(password: str) -> str: return hashlib.md5(password.encode()).hexdigest() # MD5 is broken!
import bcrypt def store_password(password: str) -> bytes: # Use bcrypt with automatic salting salt = bcrypt.gensalt(rounds=12) return bcrypt.hashpw(password.encode(), salt) def verify_password(password: str, hashed: bytes) -> bool: return bcrypt.checkpw(password.encode(), hashed)
Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection, NoSQL injection, OS command injection, and Cross-Site Scripting (XSS) are the most common forms. Hostile data can trick the interpreter into executing unintended commands.
Injection can result in data loss, corruption, unauthorized access, complete host takeover, or denial of service. SQL injection alone remains one of the most dangerous and prevalent attack vectors.
# SQL injection via string concatenation def get_user(username: str): query = f"SELECT * FROM users WHERE name = '{username}'" return db.execute(query) # username = "' OR '1'='1"
# Parameterized queries prevent SQL injection def get_user(username: str): query = "SELECT * FROM users WHERE name = %s" return db.execute(query, (username,)) # For XSS prevention, escape output from markupsafe import escape def render_comment(comment: str) -> str: return f"<p>{escape(comment)}</p>"
Insecure design refers to risks related to design and architectural flaws. It calls for the use of threat modeling, secure design patterns, and reference architectures. An insecure design cannot be fixed by a perfect implementation; the security controls needed were never created to defend against specific attacks.
Design flaws can lead to business logic vulnerabilities that are difficult to detect with automated tools. Missing rate limits on sensitive operations, lack of multi-factor authentication for critical actions, or insufficient fraud detection are all design-level failures.
# Password reset with no rate limit or verification @app.route('/reset-password', methods=['POST']) def reset_password(): email = request.form['email'] new_pass = request.form['new_password'] user = db.find_by_email(email) user.password = hash_password(new_pass) # No token verification! db.save(user)
from datetime import datetime, timedelta @app.route('/reset-password', methods=['POST']) @rate_limit("3/hour") def reset_password(): token = request.form['token'] new_pass = request.form['new_password'] # Verify time-limited, single-use token reset_req = db.find_reset_token(token) if not reset_req or reset_req.used or reset_req.expires < datetime.utcnow(): abort(400, "Invalid or expired token") # Enforce password complexity if not meets_password_policy(new_pass): abort(400, "Password does not meet requirements") reset_req.user.password = hash_password(new_pass) reset_req.used = True db.save_all([reset_req.user, reset_req])
Confirmation of a user's identity, authentication, and session management is critical. Authentication failures occur when applications allow credential stuffing, brute force, weak passwords, or have flawed session management such as non-rotating session IDs after login.
Attackers can gain access to user accounts through automated credential stuffing, brute force, or session hijacking. Compromised accounts can lead to identity theft, fraud, and data breaches.
# No brute force protection, weak session handling @app.route('/login', methods=['POST']) def login(): user = db.find_by_email(request.form['email']) if user and user.password == request.form['password']: # Plain comparison! session['user'] = user.id # Session ID not rotated return redirect('/dashboard')
from flask_limiter import Limiter limiter = Limiter(app, default_limits=["100/hour"]) @app.route('/login', methods=['POST']) @limiter.limit("5/minute") def login(): user = db.find_by_email(request.form['email']) if not user or not bcrypt.checkpw( request.form['password'].encode(), user.password_hash ): # Generic error to prevent user enumeration return "Invalid credentials", 401 # Rotate session ID after authentication session.regenerate() session['user'] = user.id # Check for MFA requirement if user.mfa_enabled: return redirect('/mfa-verify') return redirect('/dashboard')
Software and data integrity failures relate to code and infrastructure that do not protect against integrity violations. This includes insecure deserialization, using untrusted CDNs or plugins without integrity verification, and auto-update mechanisms without signed updates.
Insecure deserialization can lead to remote code execution. Loading scripts from untrusted CDNs without Subresource Integrity (SRI) can allow attackers to inject malicious code into your application.
<!-- Loading external scripts without integrity checks --> <script src="https://cdn.example.com/lib.js"></script> <!-- Insecure deserialization in Python --> import pickle data = pickle.loads(user_input) # Arbitrary code execution!
<!-- Use Subresource Integrity (SRI) for external scripts --> <script src="https://cdn.example.com/lib.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K..." crossorigin="anonymous"></script> # Use safe deserialization in Python import json data = json.loads(user_input) # Safe: only parses JSON data
Without sufficient logging, monitoring, and alerting, breaches cannot be detected in a timely manner. Insufficient logging, ineffective integration with incident response systems, and lack of real-time alerting allow attackers to further attack systems, maintain persistence, and tamper with or extract data.
Without proper logging, attackers can operate undetected for extended periods. Most breach studies show the average time to detect a breach exceeds 200 days, often discovered by external parties rather than internal monitoring.
# No logging of security-relevant events @app.route('/login', methods=['POST']) def login(): user = authenticate(request.form) if not user: return "Login failed", 401 # No record of failure return redirect('/dashboard')
import logging from datetime import datetime security_log = logging.getLogger('security') @app.route('/login', methods=['POST']) def login(): user = authenticate(request.form) if not user: security_log.warning( "LOGIN_FAILED | ip=%s | email=%s | time=%s", request.remote_addr, request.form.get('email', 'unknown'), datetime.utcnow().isoformat(), ) check_brute_force(request.remote_addr) return "Invalid credentials", 401 security_log.info( "LOGIN_SUCCESS | user_id=%s | ip=%s", user.id, request.remote_addr, ) return redirect('/dashboard')
New in 2025. Applications that improperly handle errors, exceptions, and edge cases can expose sensitive information, enter inconsistent states, or create exploitable conditions. This includes verbose error messages in production, uncaught exceptions that bypass security controls, and race conditions.
Improper error handling can expose stack traces, database details, or internal paths. Race conditions in security checks can allow time-of-check-to-time-of-use (TOCTOU) attacks, bypassing authorization or payment validation.
# Exposing internal details in error messages @app.route('/api/data') def get_data(): try: result = db.query(request.args['q']) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 # Leaks DB details!
import uuid, logging logger = logging.getLogger(__name__) @app.errorhandler(Exception) def handle_exception(e): error_id = str(uuid.uuid4()) logger.error("Unhandled exception [%s]: %s", error_id, e, exc_info=True) # Return generic error with reference ID for support return jsonify({ "error": "An internal error occurred", "reference": error_id, }), 500 @app.route('/api/data') def get_data(): q = request.args.get('q') if not q or not is_valid_query(q): return jsonify({"error": "Invalid query parameter"}), 400 result = db.query(q) return jsonify(result)
| ID | Vulnerability | Severity | Key Mitigation |
|---|---|---|---|
| A01 | Broken Access Control | Critical | Server-side access checks, deny by default, record ownership |
| A02 | Security Misconfiguration | High | Hardening process, security headers, disable defaults |
| A03 | Software Supply Chain Failures | High | Pinned dependencies, SBOM, dependency scanning |
| A04 | Cryptographic Failures | High | Strong algorithms, TLS 1.2+, key management |
| A05 | Injection | Critical | Parameterized queries, output encoding, input validation |
| A06 | Insecure Design | High | Threat modeling, secure patterns, abuse case testing |
| A07 | Authentication Failures | High | MFA, brute force protection, session rotation |
| A08 | Software or Data Integrity Failures | High | SRI, signed updates, safe deserialization |
| A09 | Security Logging & Alerting Failures | Medium | Centralized logging, real-time alerts, incident response |
| A10 | Mishandling of Exceptional Conditions | Medium | Global error handler, generic messages, TOCTOU prevention |