The average cost of a data breach reached $4.45 million in 2023, according to IBM’s annual report. For small and mid-size companies, a single breach can be existential. Yet most web applications are built with security as an afterthought, bolted on after launch rather than woven into the architecture from the start.
The uncomfortable truth is that the majority of web application vulnerabilities are not exotic zero-days. They are well-understood, well-documented flaws that the OWASP Top 10 has cataloged for over two decades: injection, broken authentication, sensitive data exposure, and misconfiguration. These vulnerabilities persist because development teams treat security as a separate discipline rather than a core engineering practice.
This guide covers the concrete security measures every web application should implement, organized by attack surface, with specific technical recommendations you can act on immediately.
Authentication and Session Management
Authentication is the front door of your application, and it is the most attacked surface. Getting it right requires attention to password handling, session management, and multi-factor authentication.
Never store passwords in plaintext or with weak hashing. Use bcrypt, scrypt, or Argon2id with appropriate work factors. Argon2id with a minimum of 19456 KiB memory, 2 iterations, and 1 degree of parallelism is the current recommendation from OWASP. These algorithms are deliberately slow, making brute-force attacks computationally expensive. MD5 and SHA-256, even with salts, are too fast for password hashing because GPUs can compute billions of SHA-256 hashes per second.
Implement account lockout with care. Locking an account after 5 failed attempts prevents brute-force attacks but creates a denial-of-service vector: an attacker can lock out any user by deliberately failing 5 login attempts. A better approach is progressive delays: the first failed attempt has no delay, the second adds a 1-second delay, the third adds 2 seconds, and so on, with a maximum delay of 30 seconds. This makes brute-force impractical without locking out legitimate users.
Session tokens must be generated using a cryptographically secure random number generator, be at least 128 bits of entropy, and be transmitted only over HTTPS. Set the Secure, HttpOnly, and SameSite=Lax (or Strict) flags on session cookies. The HttpOnly flag prevents JavaScript from reading the cookie, mitigating XSS-based session theft. The SameSite attribute prevents the cookie from being sent in cross-site requests, mitigating CSRF attacks.
Implement session timeout and rotation. Sessions should expire after a period of inactivity (15 to 30 minutes for sensitive applications, up to 24 hours for low-risk applications). Rotate session IDs after successful authentication to prevent session fixation attacks. Invalidate all sessions when a user changes their password.
Multi-factor authentication (MFA) should be available for all users and required for privileged roles. TOTP (time-based one-time passwords via apps like Google Authenticator) is the baseline. WebAuthn/FIDO2 hardware keys are the gold standard, as they are phishing-resistant because the browser verifies the origin before releasing the credential. SMS-based MFA is better than nothing but is vulnerable to SIM-swapping attacks and should not be the only option.
Input Validation and Injection Prevention
Injection attacks remain the most dangerous class of web application vulnerabilities. SQL injection, cross-site scripting (XSS), command injection, and LDAP injection all exploit the same root cause: untrusted user input is treated as code or markup.
For SQL injection, use parameterized queries (prepared statements) for every database interaction without exception. ORMs like SQLAlchemy, ActiveRecord, and Prisma use parameterized queries by default, but be vigilant about raw query escape hatches that bypass parameterization. A single Model.where("name = '#{params[:name]}'") in a codebase of 10,000 parameterized queries is all an attacker needs.
For XSS, apply output encoding appropriate to the context where user data is rendered. HTML context requires HTML entity encoding. JavaScript context requires JavaScript encoding. URL context requires URL encoding. CSS context requires CSS encoding. Using a templating engine that auto-escapes by default (React’s JSX, Django’s template engine, Jinja2 with autoescape) handles the common case, but be wary of features that bypass escaping: dangerouslySetInnerHTML in React, |safe in Jinja2, and raw in ERB.
Implement a Content Security Policy (CSP) header as a defense-in-depth measure against XSS. A strict CSP policy disallows inline scripts and restricts script sources to your own domain and trusted CDNs. Start with a report-only policy to identify violations without breaking functionality, then enforce after you have eliminated all violations:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; report-uri /csp-report
For file uploads, validate file type by inspecting magic bytes (the file header), not just the file extension or MIME type. Store uploaded files outside the web root or in an object storage service (S3) with a separate domain. Serve user-uploaded files with the Content-Disposition: attachment header and a Content-Type of application/octet-stream to prevent the browser from executing uploaded content.
API Security
Modern web applications are API-driven, and API endpoints often have weaker security than their server-rendered counterparts because developers assume API consumers are trusted.
Implement rate limiting on all API endpoints. Authentication endpoints should be limited to 5 to 10 requests per minute per IP. Data retrieval endpoints should be limited based on your typical usage patterns, often 100 to 1,000 requests per minute per authenticated user. Use a token bucket or sliding window algorithm, and return HTTP 429 with a Retry-After header when limits are exceeded.
Validate and sanitize all API input. Use schema validation libraries (Joi for Node.js, Pydantic for Python, Zod for TypeScript) to enforce type, format, and range constraints on every request body and query parameter. Reject requests that include unexpected fields to prevent mass assignment vulnerabilities.
Implement proper authorization checks at the data level, not just the endpoint level. Broken object-level authorization (BOLA), where a user can access another user’s data by changing an ID in the URL, is the number one API vulnerability according to the OWASP API Security Top 10. Every database query should include a WHERE clause that filters by the authenticated user’s permissions. Never rely solely on the obscurity of object IDs.
Use short-lived access tokens (15 minutes) with refresh tokens (7 to 30 days) for API authentication. Store refresh tokens securely (encrypted in the database, HttpOnly cookies on web clients) and implement refresh token rotation: each time a refresh token is used, issue a new refresh token and invalidate the old one. If a stolen refresh token is used after the legitimate user has already refreshed, the rotation mechanism detects the anomaly and can invalidate all tokens for that user.
See also: User Authentication and Authorization: Complete Implementation Guide
Infrastructure and Deployment Security
Application-level security means nothing if the underlying infrastructure is compromised.
Enforce HTTPS everywhere with HSTS. Redirect all HTTP traffic to HTTPS, and set the Strict-Transport-Security header with a max-age of at least one year and includeSubDomains. Submit your domain to the HSTS preload list so browsers know to always use HTTPS, even on the first visit.
Keep dependencies updated. Automated tools like Dependabot, Renovate, or Snyk can create pull requests for dependency updates and flag known vulnerabilities. Run npm audit, pip audit, or the equivalent for your package manager in CI/CD, and fail the build on high-severity vulnerabilities. Over 80% of application code is third-party dependencies; a vulnerability in a transitive dependency three levels deep is just as exploitable as one in your own code.
Configure security headers on every response. Beyond CSP and HSTS, set X-Content-Type-Options: nosniff (prevents MIME type sniffing), X-Frame-Options: DENY or use the CSP frame-ancestors directive (prevents clickjacking), and Referrer-Policy: strict-origin-when-cross-origin (limits referrer information leakage).
Implement logging and monitoring for security events. Log all authentication attempts (successful and failed), authorization failures, input validation failures, and administrative actions. Send these logs to a centralized system (ELK stack, Datadog, or Splunk) with alerting rules for anomalous patterns: a spike in failed login attempts, a sudden increase in 403 responses, or access attempts from unusual geographies.
Use environment-specific secrets management. Never store API keys, database credentials, or encryption keys in source code or configuration files that are committed to version control. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, or Doppler) that provides access controls, audit logging, and automatic rotation.
Security Testing and Continuous Assurance
Security is not a one-time activity. It is an ongoing practice that must be integrated into your development lifecycle.
Run static application security testing (SAST) in your CI pipeline. Tools like Semgrep, CodeQL, and SonarQube analyze source code for vulnerability patterns without executing it. SAST catches issues early, when they are cheapest to fix, but produces false positives that require tuning. Start with a small ruleset focused on high-confidence findings (SQL injection, hardcoded secrets, insecure cryptography) and expand over time.
Run dynamic application security testing (DAST) against your staging environment. Tools like OWASP ZAP, Burp Suite, and Nuclei send actual HTTP requests and analyze responses for vulnerabilities. DAST finds runtime issues that SAST misses: misconfigured CORS, missing security headers, and accessible debug endpoints. Schedule DAST scans weekly and after every major release.
Conduct annual penetration testing by an external security firm. Internal testing and automated tools catch the low-hanging fruit, but an experienced human tester will find business logic vulnerabilities, complex multi-step attack chains, and infrastructure weaknesses that automated tools miss. Budget $15,000 to $40,000 for a thorough web application penetration test, depending on application complexity.
Implement a vulnerability disclosure program. Publish a security.txt file at /.well-known/security.txt with a contact method for reporting vulnerabilities. Respond to reports within 48 hours, fix critical vulnerabilities within 7 days, and publicly thank researchers who report responsibly. A bug bounty program through HackerOne or Bugcrowd is a more formalized version that incentivizes external testing.
Security is not a feature you add at the end. It is a quality of the entire development process. If your web application needs a security review or you want to build security into your next project from the foundation, connect with our team to discuss your security posture.