A practical checklist in seven areas, each check with its reason in a few words, for the start of a web project and again before launch.
A secure-by-default web project is one where the safe choice is the one you get without extra effort: sessions are locked down, queries use parameters, secrets stay out of the code, and nothing ships until someone has checked. This checklist covers seven areas. Every check comes with its reason in a few words, so you can judge whether it applies to your product instead of ticking it blindly.
How to use this checklist
Run through it twice: once when the project starts, while changes are cheap, and once before launch, as a release gate. Copy the items into your tracker as tickets. For each one, record one of three answers: done, not applicable (with the reason), or accepted risk (with a named owner and a date to look again).
The list is a floor. It stops common mistakes, many of which appear in the [OWASP Top 10](/glossary/owasp-top-10), but it does not replace thinking about the rules of your own product.
Accounts and sessions
| Check | Why |
|---|---|
| Hash passwords with a purpose-built function such as Argon2id, scrypt or bcrypt. | A stolen database should not hand over every password. |
| Require [multi-factor authentication](/glossary/multi-factor-authentication) (MFA) for admin and staff accounts, and offer it to customers. | A leaked password alone is then not enough to get in. |
| Check permissions on the server for every request, against both the signed-in user and the object requested. | Hiding a button is not a control. Changing an ID in an address is the classic way in. |
| Set session cookies with HttpOnly, Secure and SameSite. | Page scripts cannot read them, they travel only over HTTPS, and most cross-site requests do not carry them. |
| Issue a new session identifier at sign-in and when privileges change, and expire idle sessions. | An identifier planted before sign-in stops working, and a forgotten tab does not stay open forever. |
| Protect requests that change data against cross-site request forgery. | A browser attaches cookies even when a hostile page triggers the request. |
| Limit sign-in and reset attempts, and make reset links single-use and short-lived. | It stops password guessing and the replay of an old email. |
Input and output
Everything that arrives from outside is input you do not control. Everything you send back lands in a place with its own rules.
- **Validate on the server with an allow-list** of type, length, range and format. Browser checks are a courtesy, because an attacker sends requests directly.
- **Use parameterised queries** or your ORM's safe methods, never SQL built from strings. This closes the door on [SQL injection](/glossary/sql-injection).
- **Encode output for the place it lands:** HTML, attribute, address or script. Rely on your framework's automatic escaping and review every raw-HTML escape hatch. This is the main defence against [cross-site scripting](/glossary/cross-site-scripting).
- **Treat uploads as hostile.** Check the file type by content, not by name, cap the size, store files outside the web root under generated names, and never execute them.
- **Restrict the addresses your server fetches for users.** Allow named hosts and block internal ones, or a link-preview feature can be turned against your own network.
- **Return only the fields the screen needs.** An API that sends a whole database record leaks whatever the interface chooses to hide.
Headers and transport
Browsers enforce a set of protections, but only when the server asks for them through response headers.
| Setting | What it does and why |
|---|---|
| HTTPS everywhere, with a redirect from HTTP, Strict-Transport-Security (HSTS), current TLS versions (1.2 and 1.3) and certificates that renew automatically | Traffic cannot be read or altered on shared networks, and browsers refuse to fall back to plain HTTP. Start HSTS with a short lifetime, then lengthen it. An expired certificate takes the site down. |
| Content-Security-Policy | Limits where scripts, styles and frames may load from, so an injected script is stopped even if a flaw slips through. |
| X-Content-Type-Options: nosniff | Stops the browser guessing a file's type, which can turn an upload into a script. |
| The frame-ancestors directive, or X-Frame-Options | Keeps other sites from embedding your pages, which is how clickjacking works. |
| Referrer-Policy: strict-origin-when-cross-origin | Keeps full addresses, which can contain tokens, out of requests your page makes to other sites. |
| CORS: name the allowed origins, and never echo back whatever origin arrives when credentials are allowed | Otherwise any website can read responses meant for your signed-in users. |
Treat headers as a safety net, not a fix. A policy does not excuse unescaped output. Roll out a [Content Security Policy](/glossary/content-security-policy) in report-only mode first, so you learn what it would break before it breaks it.
Secrets and configuration
- **Keep secrets out of the repository, including its history.** Use the host's environment variables or a secrets manager. Rotate any secret that was ever committed, because deleting the file does not delete the history.
- **Use different secrets per environment.** A key leaked from a test setup should not open production.
- **Turn on secret scanning,** and a check before each push if you can, so a mistake is caught in seconds.
- **Know what your framework sends to the browser.** In Next.js, for example, variables whose names begin with NEXT_PUBLIC_ are placed into browser code. A secret must never carry that prefix.
- **Give every account and key the least privilege it needs.** The application's database user should not be able to drop tables. A bug then does limited damage.
- **Check production defaults.** Turn off debug mode and detailed error pages, remove sample and default accounts, and keep databases and admin panels off the public internet.
Dependencies
- **Commit the lockfile and install from it in your build,** so every build uses exactly the versions you tested.
- **Scan dependencies on every pull request and on a schedule.** New flaws are published about libraries you already use.
- **Add a package for a reason.** Look at who maintains it and how recently, and ask whether a few lines of your own code would do. Every package runs with your application's access.
- **Remove what you no longer use, and pin build tools and automation steps** to exact versions. There is less to patch, and nothing changes under you without warning.
- **Decide in advance how fast a critical fix must ship,** and who owns it. When an alert arrives, ask whether the vulnerable code is actually reachable in your product.
Logging and recovery
These checks decide whether you notice an incident, what you can learn from it and how quickly you recover.
| Check | Why |
|---|---|
| Record security events: sign-ins and failures, password and MFA changes, permission changes, admin actions and refused requests. | You cannot investigate what you never recorded. |
| Keep passwords, tokens, full card numbers and personal data out of the logs. | Logs are copied to many tools and kept for a long time. |
| Send logs to a separate place the application cannot edit. | An attacker who gets into the app should not be able to erase the trail. |
| Alert on a few signals someone will act on, such as repeated failed sign-ins or a new administrator. | Logs nobody reads protect nothing. |
| Back up the data automatically, and test a restore. | A backup you have never restored is a hope, not a plan. |
| Write a one-page incident plan: who decides, who is called, how keys and sessions are revoked, how users are told. | The first hour goes better when the choices were made earlier. |
The release gate
Before launch, turn the checklist into a short sign-off that someone other than the code's author can answer with yes.
This is also our method for the products we build: every project starts with a threat review, and a security review before launch hands you a prioritised fix list. The [cybersecurity service](/services/cybersecurity) page describes how that works.
What this checklist does not cover
A checklist catches known mistakes. It does not find a flaw in your own rules, such as a discount that can be applied twice. [Threat modelling](/glossary/threat-modelling) looks at that design level, and a [penetration test](/blog/penetration-test-vs-vulnerability-scan) checks the running system.
It also cannot tell you which laws apply to your data. Rules on personal data differ by country and sector, so confirm what applies with qualified counsel. For the wider picture, from requirements to release, read the [guide to secure app development](/guides/secure-app-development).