Web applications are the most targeted attack surface in modern infrastructure. According to Verizon's annual Data Breach Investigations Report, web application attacks account for the majority of confirmed breaches year after year. Despite this, many development teams still ship code with preventable vulnerabilities baked in from day one.
This checklist is not a theoretical overview. It is a working reference built for developers, security engineers, and DevSecOps teams who want to ship secure code without slowing down delivery pipelines. Every section covers the control, explains the risk, and provides real commands or code you can use today.
If your team needs a structured external review of your application's security posture, the application security services at Redfox Cybersecurity are designed to complement exactly this kind of internal hardening process.
Injection remains one of the most exploited vulnerability classes because developers continue to mix untrusted data with code logic. SQL injection, command injection, LDAP injection, and template injection all share the same root cause: unvalidated input reaching an interpreter.
Every input entering your application, regardless of source, must be validated against a strict allowlist before processing.
Server-Side Validation in Node.js Using Joi:
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).max(120).required()
});
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
[cta]
Reflected and stored XSS attacks succeed when user-controlled data is rendered in a browser without encoding. Use context-aware encoding for every output context, HTML body, HTML attributes, JavaScript, and URL parameters.
Using DOMPurify for Client-Side Sanitization:
import DOMPurify from 'dompurify';
const userContent = DOMPurify.sanitize(rawInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: []
});
document.getElementById('output').innerHTML = userContent;
[cta]
For server-side rendering in Python, use the markupsafe library to escape all dynamic values before injecting them into HTML templates.
from markupsafe import escape
safe_output = escape(user_supplied_string)
[cta]
Weak password hashing is still responsible for mass credential exposure after breaches. MD5 and SHA-1 are not acceptable for password storage in 2026. Use bcrypt, Argon2id, or scrypt with a work factor tuned to your server capacity.
Argon2id Password Hashing in Python:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hashed = ph.hash("user_plaintext_password")
try:
ph.verify(hashed, "user_plaintext_password")
print("Password valid")
except VerifyMismatchError:
print("Invalid password")
[cta]
MFA adoption remains the single most effective control against credential-based attacks. For applications handling sensitive data, TOTP-based MFA should be a hard requirement, not an optional setting.
For greenfield applications in 2026, consider implementing WebAuthn (passkeys) as a primary or fallback authentication method. The FIDO2 standard eliminates phishing risk at the protocol level.
Verifying a TOTP Token in Python Using PyOTP:
import pyotp
totp = pyotp.TOTP("BASE32_SECRET_HERE")
if totp.verify(user_submitted_token, valid_window=1):
print("MFA verified")
else:
print("Invalid or expired token")
[cta]
Tokens must be generated using a cryptographically secure random source, bound to a single session, invalidated on logout, and rotated after privilege escalation.
Set these attributes on every session cookie:
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
[cta]
Never store session identifiers in localStorage or sessionStorage. These are accessible to JavaScript and therefore vulnerable to XSS-based session hijacking.
Broken object-level authorization (BOLA) and broken function-level authorization are consistently among the top OWASP API Security risks. Every API endpoint must independently verify both authentication and authorization, regardless of how the request was routed.
Express.js Middleware for JWT Verification:
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.get('/api/account/:id', authenticateToken, (req, res) => {
if (req.params.id !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
// proceed to fetch account
});
[cta]
Unauthenticated and authenticated endpoints alike need rate limiting to prevent credential stuffing, enumeration, and denial-of-service conditions. Use a token bucket or sliding window algorithm, not a simple per-minute counter.
Rate Limiting with express-rate-limit:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many login attempts. Try again later.' }
});
app.post('/api/auth/login', loginLimiter, loginHandler);
[cta]
Teams building or auditing APIs at scale can benefit from a dedicated API security review. Redfox Cybersecurity's penetration testing and API security services cover BOLA, BFLA, mass assignment, and injection testing against REST, GraphQL, and gRPC interfaces.
Security headers are one of the highest-ROI controls available. They are configured once and harden your application against entire classes of client-side attacks.
Recommended Header Configuration (Nginx):
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always;
[cta]
A well-crafted CSP eliminates the exploitability of most XSS vulnerabilities by restricting where scripts, styles, and resources can be loaded from. Start with a blocking policy in report-only mode and tighten iteratively.
CSP Report-Only Header for Baselining:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; report-uri /csp-report-endpoint;
[cta]
Monitor the reports, identify legitimate sources being blocked, and promote to enforcement mode once violations drop to zero.
The average modern web application pulls in hundreds of transitive dependencies. Each one is a potential attack surface. The npm and PyPI ecosystems have seen a steady increase in malicious package uploads, typosquatting, and dependency confusion attacks.
Running a Dependency Audit:
# Node.js
npm audit --audit-level=high
# Python using pip-audit
pip install pip-audit
pip-audit --desc
# Ruby
bundle audit check --update
[cta]
Use lockfiles and subresource integrity (SRI) hashes for any assets loaded from a CDN. Never load scripts from third-party origins without an integrity check.
SRI Hash for a CDN-Loaded Script:
<script
src="https://cdn.example.com/library.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux6J6sF5ZqF6M8l6qJ8lS6+zxM0W"
crossorigin="anonymous">
</script>
[cta]
Generate SRI hashes using the openssl command or the srihash.org utility. Automate this into your CI pipeline so new CDN assets are never deployed without a verified hash.
Tools like OWASP Dependency-Check, Snyk, and Grype provide automated SCA scanning. Embed them as a gate in your pipeline so builds fail on critical CVEs.
Running Grype Against a Container Image:
grype my-app:latest --fail-on critical
[cta]
Hardcoded API keys, database credentials, and private keys in source code are discovered by attackers using automated GitHub and GitLab scanning within minutes of a repository going public. This applies equally to private repositories that are later misconfigured.
Scanning for Secrets Before Commit Using Gitleaks:
# Install
brew install gitleaks
# Scan a repository
gitleaks detect --source . --verbose
# Use as a pre-commit hook
gitleaks protect --staged
[cta]
Inject secrets at runtime from a vault, not from environment files checked into source control. HashiCorp Vault, AWS Secrets Manager, and GCP Secret Manager all provide auditable, rotatable secret storage with fine-grained access policies.
Fetching a Secret from AWS Secrets Manager in Python:
import boto3
import json
client = boto3.client('secretsmanager', region_name='us-east-1')
response = client.get_secret_value(SecretId='prod/myapp/db-credentials')
secret = json.loads(response['SecretString'])
db_password = secret['password']
[cta]
Production environments must never expose stack traces, database query details, or internal file paths in HTTP responses. These details are primary reconnaissance aids for attackers.
Django Production Settings Checklist:
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com']
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': '/var/log/django/error.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
},
}
[cta]
Every application endpoint must use TLS 1.2 at minimum, with TLS 1.3 preferred. Disable SSLv3, TLS 1.0, and TLS 1.1 at the server level. Use Mozilla's SSL Configuration Generator as a baseline and verify the result with testssl.sh.
Testing TLS Configuration with testssl.sh:
./testssl.sh --severity HIGH --warnings off https://yourdomain.com
[cta]
Automate certificate renewal using Certbot or an ACME client integrated with your load balancer. Expired certificates are an availability risk and create opportunities for MITM attacks when users click through browser warnings.
Static application security testing tools analyze source code for vulnerability patterns without executing the code. Integrate them into your IDE and your CI pipeline so developers get feedback before code is merged.
Running Semgrep Against a Python Codebase:
pip install semgrep
semgrep --config=p/owasp-top-ten --config=p/python ./src
[cta]
Running Bandit for Python Security Linting:
pip install bandit
bandit -r ./src -l -ii
[cta]
DAST tools probe running applications the way an attacker would, without access to source code. OWASP ZAP and Nuclei are strong open-source options for automated scanning. Pair them with manual testing for coverage that automated tools consistently miss.
Running a Nuclei Scan Against a Target Application:
nuclei -u https://staging.yourdomain.com \
-t cves/ \
-t exposures/ \
-t misconfiguration/ \
-severity medium,high,critical \
-o nuclei-results.txt
[cta]
For applications handling financial data, PII, or healthcare records, automated scanning is a starting point, not a complete program. Redfox Cybersecurity's web application penetration testing services provide the manual testing depth that uncovers business logic flaws, chained vulnerabilities, and access control issues that scanners routinely miss.
Comprehensive logging enables detection of attacks in progress and supports forensic analysis after a breach. Log authentication events, authorization failures, input validation failures, and high-value transactions. Never log passwords, session tokens, credit card numbers, or other sensitive data.
Structured Logging in Node.js Using Winston:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
logger.info('Login attempt', {
userId: user.id,
ip: req.ip,
userAgent: req.headers['user-agent'],
success: false
});
[cta]
Ship logs to a centralized SIEM or log management platform. Set up alerts for brute-force patterns, privilege escalation events, unusual data export volumes, and access from unexpected geographies. Tools like Elastic SIEM, Grafana Loki with AlertManager, or commercial platforms like Panther provide the detection layer your application logs need.
Web application security in 2026 is not a checkbox exercise completed before launch. It is a continuous practice embedded into every phase of the development lifecycle.
The controls covered in this checklist, input validation, secure authentication, API authorization enforcement, HTTP security headers, dependency scanning, secrets management, TLS hardening, SAST and DAST integration, and structured logging, form a layered defense that raises the cost of attacks significantly.
No single control is sufficient on its own. Attackers chain weaknesses. A missing rate limit combined with a verbose error message combined with a weak session token is a fully exploitable authentication bypass. Defense in depth is not optional.
If your team is ready to validate how these controls perform against a skilled adversary, the web application security team at Redfox Cybersecurity provides structured penetration testing, code review, and DevSecOps consulting tailored to teams building production applications at scale.