All articles

BLOG / WEB APP SECURITY

10 Web App Security Best Practices: A Developer Guide

Learn the web app security best practices that matter most for signups, logins, and checkouts, with steps, trade-offs, and sources.

You're probably looking at a login, checkout, or account recovery flow that mostly works, but you also know it's carrying more risk than the product team wants to admit. Attackers don't need a dramatic exploit to cause trouble. A reused password, a missing authorization check, or a session cookie that never gets rotated can be enough to reach the actions that matter most. If you want a practical starting point, reduce your breach risk in 2026 and then come back to the controls that protect your highest-value pages first.

Web app security best practices work best when teams roll them out in order. Start with transport, identity, and authorization. Then move through input handling, session and request integrity, browser hardening, dependency control, and abuse defenses. The goal isn't perfect protection, because that doesn't exist. The goal is to make the common attack paths expensive, noisy, and hard to automate.

Table of Contents

1. Serve Everything Over HTTPS and Enforce It With HSTS

If a browser is still speaking plain HTTP, the rest of your controls are playing catch-up. HTTPS encrypts traffic in transit, which keeps login forms, checkout details, and session cookies away from anyone on the network path. HSTS, or HTTP Strict Transport Security, tells browsers to use HTTPS automatically for your domain, even when someone types an old ` link.

The rollout trade-off is simple. HTTPS is basic hygiene, but HSTS is easy to misapply if you haven't mapped every subdomain. A bad includeSubDomains rollout can lock out a forgotten host, and mixed content can break secure pages in ways that are annoying to debug.

A digital illustration showing a shield with HSTS text behind a browser address bar showing https.

Start conservatively, then lock it down

Practical rule: ship HSTS with a short max-age first, verify redirects and subdomains, then increase the lifetime after you've confirmed nothing still depends on HTTP.

A few specifics matter in real deployments:

  • Redirect every HTTP URL to HTTPS: Don't leave old paths open because some crawler or bookmark still uses them.
  • List subdomains before includeSubDomains: Forgotten hosts cause the painful failures, not the main app.
  • Remove mixed content: A secure page that loads images, scripts, or fonts over HTTP gives attackers a weak link to work with.
  • Set HSTS only on HTTPS responses: Browsers ignore it over plain HTTP anyway.

Chrome, Firefox, and Safari all maintain HSTS preload lists for major sites, and Let's Encrypt has helped make certificate adoption practical at scale since 2016. For many teams, the first step this week is to confirm that the app and every related domain redirect cleanly to HTTPS, then add HSTS with a small max-age and verify the result in production.

2. Use Modern Password Rules, Breached-Password Checks, and MFA on Sensitive Logins

A weak login flow is usually where account takeover starts. Use long passwords, check new passwords against breached-password lists, and stop forcing periodic resets or rigid composition rules that push people toward predictable patterns. Let users pick strong passphrases, then reject passwords attackers already know.

MFA, or multi-factor authentication, adds a second proof at login. A stolen password should not be enough for signup, sign-in, password reset, or account recovery. For small-team rollout context, see this MFA guide for Canadian SMBs when planning help-desk coverage.

The trade-off is friction. A careful rollout reduces account-takeover risk, but a clumsy one raises support tickets and recovery requests. Recovery codes, generic error messages, and logging matter as much as the second factor itself, because users forget devices and attackers probe the recovery path.

Use the same bar across the entire account lifecycle. Signup, password change, reset, and recovery should follow the same rules. If one path is weaker, attackers move there.

A practical rollout usually looks like this:

  • Hash passwords correctly: Use a slow, purpose-built algorithm such as Argon2id, scrypt, or bcrypt, and never store plain text.
  • Check against breached-password data: Have I Been Pwned's Pwned Passwords service supports k-anonymity range queries, so you do not have to send the full password.
  • Prefer passkeys or authenticator apps: SMS is better than nothing, but it is not the strongest option.
  • Hide account existence: Return generic login errors so attackers cannot confirm which email address is valid.
  • Pair MFA with bot signals: Login pages are a common target for automated guessing and credential stuffing attacks.

If your login flow already handles password resets cleanly, the first step this week is to add breached-password checks, then turn on MFA for admin, finance, and support accounts. That sequence keeps the rollout focused on the accounts that would hurt most if they were taken over.

3. Enforce Authorization on the Server for Every Request

Authentication answers who the user is. Authorization answers what they're allowed to do. Teams get into trouble when they confuse the two, or when they assume a hidden button, a client-side check, or an unguessable URL is enough.

Broken access control was the leading web-application security risk in OWASP Top 10:2021, moving up from fifth place in 2017, and OWASP's testing data tied that category to more than 318,000 recorded occurrences across 34 CWEs. That's a strong hint that this isn't an edge case. It's a core design problem.

An insecure direct object reference, or IDOR, is the classic failure mode. A user changes an order ID, workspace ID, or reservation number and suddenly sees someone else's data because the server never checked ownership. The fix is boring and reliable, which is exactly what you want here: check permissions on the backend for every read, write, and delete.

Test the permission boundary, not the button

If a UI hides a control, assume nothing. Security has to live on the server.

Two test accounts should never be able to reach each other's objects, even if the client changes IDs, replays requests, or removes a field the UI normally sends.

A practical rollout means:

  • Derive identity from the session or token: Don't trust userId fields in the request body.
  • Write authorization tests: Use two accounts and try to read, update, and delete each other's objects.
  • Use UUIDs as a supplement, not a fix: Random-looking IDs reduce guessing, but they don't enforce ownership.
  • Log denied requests: Repeated probing should show up in monitoring, not disappear into application noise.

Ticketing systems, SaaS workspaces, and admin panels all hit the same bug class in different ways. A buyer should be able to manage only their reservation. A tenant should only see their own records. The server should prove that relationship every time.

4. Prevent Injection With Parameterized Queries and Strict Input Validation

Injection starts when an application treats user input like executable code. SQL injection gets the most attention, but the same mistake shows up in shell commands, templates, and any place where untrusted input reaches an interpreter. The fix is simple in principle, and easy to miss in a mixed codebase. Keep values separate from commands so the driver, template engine, or runtime handles them as data.

Prepared statements do that separation. Input validation adds a second layer by checking that a value matches the expected type, length, and format before it reaches business logic. Validation does not stop injection on its own, but it catches bad input early and cuts down the attack surface.

OWASP ranked injection third in 2021, and its data showed mapped injection CWEs appearing approximately 274,000 times. That is still enough to make this a routine engineering problem, not a special case.

Push the checks to the server

Client-side validation helps the user. It does not protect the backend.

A good rollout starts at the API boundary. Search for string concatenation in SQL, shell, and template code first, because those are the paths that turn input into commands. Then set request-size limits on routes that handle sensitive flows like checkout or account settings. Large payloads are a common way to stress parsers and hide malformed fields. Add static analysis in CI so unsafe query patterns show up before merge instead of after release.

Many Next.js and Node.js teams pair Prisma or Drizzle with schema validation libraries such as Zod. That gives the route a clear shape and rejects malformed input early. It still does not replace parameterization. It just makes the boundary cleaner before the query layer sees the request.

The trade-off is discipline. Parameterized queries are routine once the team gets used to them, but older codebases often hide one or two string-built queries in edge paths, admin tools, or quick fixes. Those are the places to audit first, because they usually survive reviews and fail only when someone probes them with unexpected input.

5. Harden Session Cookies and Rotate Session IDs at Login

A session cookie is the browser's proof that later requests belong to the same user. If an attacker steals it, they can act as that user until the session expires or gets invalidated. Cookie flags and session rotation reduce that risk.

HttpOnly keeps JavaScript from reading the cookie. Secure tells the browser to send it only over HTTPS. SameSite limits when browsers attach it to cross-site requests. Session fixation is the related failure mode, where the server keeps the same session ID after login and an attacker who already knows that ID gains access once the user authenticates.

OWASP's session guidance recommends generating a new session token after authentication or re-authentication and terminating the old token. NIST also requires session-binding secrets to be established during or immediately after authentication, generated with an approved random-bit generator, and contain at least 64 bits of entropy. In practice, login should assign privilege to a fresh token, not reuse the anonymous session already sitting in the browser.

Browser defaults help, but they do not replace explicit settings. Chrome made SameSite=Lax the default for cookies without a SameSite attribute starting with Chrome 80 in 2020, which pushed teams to set it on purpose instead of assuming the browser would do the right thing.

Rotate the session again when trust changes. A password reset, MFA device change, or move into a more sensitive role should invalidate the old token. A stale session should not survive an account-level change.

Keep the rules direct:

  • Set cookie attributes directly: Do not rely on defaults.
  • Use __Host- prefixes where possible: That binds the cookie to a single host over HTTPS.
  • Expire sessions on logout and inactivity: Long-lived cookies are convenient and risky.
  • Keep access tokens short-lived: Store refresh tokens server-side when you can.

Next.js teams often set session cookies in route handlers or middleware with httpOnly, secure, and sameSite options. That approach keeps the protection close to the auth flow instead of spreading cookie logic across the app, which makes later audits much easier.

6. Protect State-Changing Requests From Cross-Site Request Forgery

A logged-in user opens a malicious page in another tab, and the browser sends a request your app trusts. The session cookie goes along for the ride. That is enough to change an email address, submit a transfer, or update a shipping address if the server accepts the request at face value.

The fix is layered. CSRF tokens prove the request came from the current session. SameSite cookies cut off some cross-site requests before they reach your app. Checking the Origin or Referer header gives you one more signal on sensitive routes. CORS is a separate control, and it is easy to confuse the two. CORS decides which origins can read API responses. It does not decide whether a forged form submission should be accepted.

For higher-risk actions, require the user to re-enter current credentials before you change sensitive account data or complete a high-impact transaction. OWASP recommends that for changes such as a password or email update, and for sensitive actions such as a shipping address change. The trade-off is clear. You add friction where the risk justifies it, but you also close the gap left by an already-authenticated browser session.

Make the state change prove it belongs

A solid rule is simple. A state-changing request should prove the current session, the current origin, and, for high-impact actions, recent re-authentication.

Use the controls in the right places:

  • Use CSRF tokens on cookie-backed forms: Checkout flows and account settings need them.
  • Treat GET as read-only: Put changes behind POST or another unsafe method.
  • Lock down CORS lists: Never pair Access-Control-Allow-Origin: * with credentials.
  • Check origin headers on sensitive routes: Missing or unexpected values deserve review.

Django and Rails both include built-in CSRF protection for form submissions. That is the right default for most server-rendered apps. Custom glue code only makes sense when a real client flow needs it, such as a mobile or embedded integration that cannot follow the same browser path. Those cases need testing, because a control that blocks legitimate requests creates its own support burden.

7. Limit XSS With Output Encoding and a Content Security Policy

A search box, comment field, profile bio, or URL parameter can become an XSS vector if the app reflects it without escaping. Once attacker-controlled JavaScript runs in a browser session, it can read page data, submit forms, or rewrite a checkout screen before the user notices.

Start with output encoding. Most modern frameworks escape content by default, but teams break that protection when they inject raw HTML or reach for DOM APIs like innerHTML. CSP is the second layer. It tells the browser which scripts, styles, and network connections are allowed, and it can contain damage if one rendering bug slips through.

Google's web.dev guidance on strict CSP recommends nonce-based policies instead of hostname allow-lists, since allow-lists are easier to bypass. Next.js documentation also shows how to generate per-request nonces in middleware for strict CSP support. That approach keeps trust tied to the current response, not to a broad host pattern that may age badly as the app changes.

A CSP also needs a rollout plan.

Use report-only mode before enforcement

A strict policy can break real pages quickly, especially in older codebases with inline scripts and ad hoc DOM manipulation. Start in report-only mode, then watch what fails before you block anything.

  • Audit every use of innerHTML and eval: These are common escape hatches around framework protections.
  • Avoid unsafe-inline and unsafe-eval: They weaken the policy in ways attackers like.
  • Sanitize rich text before rendering: DOMPurify is a common choice for user-supplied HTML.
  • Review violation reports: They show where the app still depends on risky behavior.

The trade-off is developer convenience. Strict CSPs require more care, and they surface legacy patterns that worked before security was tightened. That friction is acceptable on pages that handle identity, payment, or other flows the business cannot afford to expose.

8. Protect High-Value Actions From Automated Abuse Without Adding CAPTCHAs

A login form, signup page, checkout flow, or inventory hold can be expensive even when the code is correct. Scripts and headless browsers can create fake accounts, try stolen passwords, replay checkout requests, and drain limited stock faster than a normal user can react. Rate limits help. IP blocking helps too. Neither one stops an attacker who rotates addresses and behaves like a browser.

That is the gap a browser verification layer can fill. MANDATE collects browser proof on the client, checks it on the server, and lets teams allow, flag, or block requests on signup, login, checkout, and similar high-value actions without putting a challenge in front of every user. Its anti-bot verification flow is meant for the abuse boundary. It does not replace authorization or session handling.

The trade-off is operational, not technical. If you switch to blocking too early, support will absorb the mistakes. If you leave it in observe mode for too long, fraud keeps adapting and the signal gets weaker as a control.

Put verification at the action boundary

The best place for this control is where abuse turns into cost. Account creation, password reset, checkout, and inventory holds usually matter more than a generic homepage or product page. That is where bots hit revenue, support queues, and inventory integrity.

A practical rollout starts with measurement, not enforcement. Keep the decision attached to the request, and rate-limit by account and session as well as by IP address so rotating infrastructure does not erase the signal. Then review the log next to the action, because fraud teams need the context later.

Start in Observe mode. Test on real browsers, including Chrome, Firefox, Safari, and mobile, on staging. Define a fallback path so checkout does not fail if the service is unavailable. Pair the signal with other controls, since browser proof, rate limits, and server-side authorization each cover a different failure mode.

For teams that want a reference point, anti-bot verification works as one layer of defense, not the whole answer. It helps most when it sits at the boundary of a sensitive action and is rolled out with clear logs, a fallback path, and staged enforcement.

9. Manage Dependencies, Secrets, and Third-Party Scripts

A web app can be solid at the edge and still fail because of what it loads behind the scenes. Frameworks, libraries, build tools, analytics tags, payment widgets, and support scripts all sit between the browser and your data. If one of them is outdated, compromised, or over-privileged, production feels it fast.

OWASP classifies vulnerable and outdated components as a common risk, and Log4Shell showed how one widely used dependency can reach a lot of systems. Secrets create a different failure mode. API keys, database passwords, and signing keys end up in source code, client bundles, or logs when teams do not keep them in a dedicated system.

Treat vendor code like application code. A third-party script can read the DOM and anything else on the page, so a tag that can see checkout fields can also see checkout fields.

  • Run dependency audits in CI: Set a patch policy for critical findings.
  • Keep secrets out of the browser: Watch for public environment variable prefixes in build tools.
  • Use SRI where it fits: Subresource Integrity helps for third-party scripts that do not change often.
  • Review vendor data handling: New tags should earn their place.
  • Rotate leaked secrets immediately: Even a private repo leak counts.

GitHub secret scanning and push protection help catch common API key formats before code gets pushed. That does not catch every leak, but it does remove a lot of avoidable ones. The trade-off is maintenance work. Patching dependencies and reviewing scripts takes time, yet that is cheaper than discovering a compromised package has been running in production for weeks.

10. Log Security Events and Roll Out New Controls in Observe Mode First

A rollout goes sideways fast when a control starts blocking real users with no warning. Logging shows failed logins, denied permissions, rate-limit hits, and verification failures. Monitoring turns those events into a decision point instead of a mystery.

The usual mistake is to flip on enforcement too early. A rule that looks fine in staging can break live traffic, especially when region, device mix, accessibility needs, or legitimate automation behave differently in production. A safer pattern is to observe first, as described in observe-before-enforce, compare the would-have-blocked result with real traffic, and then enforce one flow at a time.

MANDATE supports that pattern with Observe mode and a dashboard for events, decisions, and protection activity. The rollout question is simple, can you block the behavior without creating a support problem? The harder part is proving it before users feel it.

A payment team might review flagged checkout sessions against refund and chargeback cases during tuning. An auth team might pair login logs with rate-limit spikes so a burst of failed attempts goes to review first, not straight to a hard stop.

Practical rule: never log passwords, full session tokens, card numbers, or raw verification payloads. Hash or truncate identifiers and keep the logs useful, not dangerous.

Use a rollout model that can absorb mistakes:

  • Define the observe window: Decide in advance what has to look stable before enforcement.
  • Change one flow at a time: Start with login, then checkout, then other high-value actions.
  • Keep rollback out of the deploy path: If the integration supports it, switch modes without a redeploy.
  • Review false positives by segment: Geography, device type, accessibility needs, and legitimate automation all need separate review.
  • Bring in extra review capacity early: Teams that need more hands on tuning and on-call checks can hire developers from latam during the observe window.

Observe mode buys time to tune thresholds and catch edge cases. It does not replace incident response, and it does not fix a bad rule on its own. It gives you a feedback loop, then enforcement can follow with less surprise.

Web App Security: 10 Best-Practices Comparison

Practice Implementation Complexity πŸ”„ Resource Requirements ⚑ Expected Outcomes β­πŸ“Š Ideal Use Cases πŸ’‘ Key Advantages ⭐
1. Serve Everything Over HTTPS and Enforce It With HSTS Low–Moderate πŸ”„, enable TLS, redirects, and HSTS headers TLS certs, edge/load-balancer config; free CAs available ⚑ Strong transport encryption; prevents passive snooping and many downgrade attempts β­πŸ“Š All public sites, login and checkout flows πŸ’‘ Baseline security, low cost, widely supported ⭐
2. Modern Password Rules, Breached-Password Checks, and MFA Moderate πŸ”„, add checks, MFA flows, and recovery UX Auth backend changes, breached-password service, MFA providers, UX work ⚑ Fewer account takeovers; blocks credential-stuffing and replay attacks β­πŸ“Š User accounts, sensitive logins, admin portals πŸ’‘ Stronger authentication; phishing-resistant options (passkeys) ⭐
3. Enforce Authorization on the Server for Every Request High πŸ”„, per-endpoint checks, RBAC/ABAC, centralized policies Development effort, testing, caching/DB indexes; possible infra changes ⚑ Prevents unauthorized data access (IDORs); improves auditability β­πŸ“Š Multi-tenant SaaS, apps with user-specific data πŸ’‘ Stops broken access control; central policy enforcement ⭐
4. Prevent Injection With Parameterized Queries and Validation Moderate πŸ”„, adopt prepared statements and schema validation Dev updates, ORMs/sanitizers, static analysis tools ⚑ Eliminates most SQL/command injection risks; clearer client errors β­πŸ“Š Any app interacting with DBs, shell, or templates πŸ’‘ Reliable, low-overhead defense; predictable validation ⭐
5. Harden Session Cookies and Rotate Session IDs at Login Low–Moderate πŸ”„, set cookie flags, rotation, and timeouts Session store, config changes, session invalidation logic ⚑ Reduces session theft, fixation, and CSRF exposure β­πŸ“Š Authenticated apps and sensitive session flows πŸ’‘ Limits impact of stolen cookies; enables revocation ⭐
6. Protect State-Changing Requests From CSRF Moderate πŸ”„, implement tokens/origin checks and CORS allow-list Server/client token handling, CORS config, SameSite tuning ⚑ Prevents forged cross-site state changes and unintended actions β­πŸ“Š Form submissions and cookie-auth APIs πŸ’‘ Blocks cross-site exploits with minimal UX impact (when tuned) ⭐
7. Limit XSS With Output Encoding and a Content Security Policy High πŸ”„, enforce escaping, CSP nonces/hashes, audit third-party scripts Dev fixes, CSP tuning and reporting, possible refactors ⚑ Significantly reduces XSS and limits impact of injected scripts β­πŸ“Š Content-heavy sites and user-generated content platforms πŸ’‘ Framework escaping + CSP reduces attack surface and damage ⭐
8. Protect High-Value Actions From Automated Abuse (Invisible Verification) Moderate πŸ”„, integrate client SDK, server verification, stage rollout Third‑party service/SDK, edge/middleware integration, observability ⚑ Fewer scripted attacks while preserving user conversion and UX β­πŸ“Š Signup, login, checkout, ticketing, inventory-limited flows πŸ’‘ Invisible verification with server-side decisions; low friction ⭐
9. Manage Dependencies, Secrets, and Third-Party Scripts Moderate πŸ”„, SCA, secret managers, SRI, CI scanning and policies Tooling, processes, maintenance, reviewer time, CI integration ⚑ Fewer vulnerable components and reduced secret-exposure incidents β­πŸ“Š Projects with many deps, client bundles, and third-party tags πŸ’‘ Prevents supply-chain and secret-leak risks; reproducible builds ⭐
10. Log Security Events and Roll Out Controls in Observe Mode First Moderate πŸ”„, structured logging, dashboards, staged enforcement Logging/monitoring stack, alerting, staff for tuning and review ⚑ Improved visibility; safer rollouts and fewer false blocks β­πŸ“Š Teams deploying new controls or protecting high-risk flows πŸ’‘ Data-driven tuning; reduces customer impact from misconfiguration ⭐

Turn This List Into a Ranked Rollout Plan

Start with the controls that protect your highest-value actions, such as login, password reset, and checkout. That usually means HTTPS, password and MFA policy, server-side authorization, and session handling before anything else. If those flows are weak, fancy browser defenses won't save you, because attackers will just take the simplest route through the app.

Test each change with two accounts and watch what happens before you enforce it. Use one account to act normally and the other to probe boundaries, replay requests, and try object switching. That simple habit catches more mistakes than a long policy document, because it forces the team to see how the app behaves in practice.

Revisit the list after every major release. New endpoints, new scripts, and new integrations add risk over time, even when the code review looked clean. Dependency updates, third-party tags, and new checkout paths deserve the same scrutiny as the original launch.

Where automated abuse is the main problem, MANDATE's invisible verification can sit alongside the rest of the stack. It's a useful layer for signup, login, checkout, and similar actions, but it doesn't replace authorization, session handling, secure coding, or input validation. The strongest setup is still layered, measured, and rolled out in stages.


If you're protecting logins, checkouts, or account recovery flows, MANDATE gives you invisible browser verification that fits into the abuse layer without adding CAPTCHAs. Use MANDATE to review decisions in Observe mode first, then decide where allow, flag, or block makes sense for your highest-value actions.

More from the blog

INGRESS VS EGRESS TRAFFIC

Ingress vs Egress Traffic: A Practical Guide for Web Apps

Learn the real difference between ingress vs egress traffic, why the directions matter for security and cost, and where to verify browser requests

BOT ATTACKS

Bot Attacks: A Complete Guide for Developers

Learn what bot attacks are, how to detect automated abuse, and practical mitigation strategies that protect signups and checkouts without CAPTCHAs.

ANTI BOT VERIFICATION

Anti Bot Verification Explained Without CAPTCHAs

Learn what anti bot verification is, how invisible browser verification works, and how it compares to CAPTCHAs for signups, logins, and checkout.