Skip to main content
Back to blog
March 17, 2026·Updated May 2, 2026|10 min read|Antoine Duno|Web Security

Website Security Checklist: 20 Checks Every Developer Must Run Before Launch

Most security incidents are caused by preventable configuration gaps, not sophisticated zero-day exploits. This 20-point checklist covers every layer of web application security that needs to be verified before you push to production, with brief explanations and remediation steps for each item.

Antoine Duno

1,824 words

AD

Antoine Duno

Founder of ZeriFlow · 10 years fullstack engineering · About the author

Key Takeaways

  • Most security incidents are caused by preventable configuration gaps, not sophisticated zero-day exploits. This 20-point checklist covers every layer of web application security that needs to be verified before you push to production, with brief explanations and remediation steps for each item.
  • Includes copy-paste code examples and step-by-step instructions.
  • Free automated scan available to verify your implementation.

Website Security Checklist: 20 Checks Every Developer Must Run Before Launch

Most web application breaches are not caused by sophisticated nation-state attacks or undiscovered zero-days. They happen because someone shipped to production without enabling HTTPS, left an API key in a public GitHub repo, or forgot to add rate limiting to a login endpoint.

This website security checklist covers 20 concrete checks across every layer of your stack. Work through it before any major launch and keep it as part of your release process. Each item includes a brief explanation of why it matters and what to do if you fail the check.


Transport Layer (TLS/HTTPS)

1. HTTPS is enforced on all pages

Why it matters: If any page of your site loads over plain HTTP, credentials, session tokens, and form data are transmitted in clear text and can be intercepted on the network.

Check: Visit http://yourdomain.com and confirm it redirects to HTTPS. Check every subdomain.

Fix: Configure a permanent (301) redirect from HTTP to HTTPS at the server or CDN level. Get a free certificate from Let''s Encrypt if you do not have one.


2. TLS certificate is valid and not expiring within 30 days

Why it matters: An expired certificate causes browser warnings that drive users away and can trigger security alerts in monitoring tools. Expiry-driven downtime is entirely preventable.

Check:

bash
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates

Fix: Set up automated certificate renewal (Let''s Encrypt with Certbot or ACME client, Cloudflare auto-renew). Set a calendar reminder 60 days before manual certificate expiry.


3. TLS version is 1.2 or 1.3 only

Why it matters: TLS 1.0 and 1.1 are deprecated and contain known vulnerabilities (BEAST, POODLE). Accepting them exposes users to downgrade attacks.

Check:

bash
nmap --script ssl-enum-ciphers -p 443 yourdomain.com

Fix: In Nginx: ssl_protocols TLSv1.2 TLSv1.3;. In Apache: SSLProtocol -all +TLSv1.2 +TLSv1.3.


4. HSTS header is configured

Why it matters: Without HSTS, attackers on the same network can intercept the initial HTTP request before your redirect, performing an SSL stripping attack.

Check: curl -I https://yourdomain.com | grep -i strict-transport

Fix: Strict-Transport-Security: max-age=31536000; includeSubDomains


HTTP Security Headers

5. Content Security Policy (CSP) is present and enforced

Why it matters: CSP is the primary defence against XSS. Without it, injected scripts can load resources from any domain, steal cookies, or exfiltrate data.

Check: curl -I https://yourdomain.com | grep -i content-security-policy

Fix: Start with Content-Security-Policy-Report-Only: default-src ''self''; report-uri /csp-reports, collect violations, then switch to enforcement.


6. X-Frame-Options or CSP frame-ancestors is set

Why it matters: Without framing protection, attackers can embed your site in an iframe, overlay it with a transparent decoy page, and capture user clicks or keystrokes (clickjacking).

Check: curl -I https://yourdomain.com | grep -i x-frame

Fix: X-Frame-Options: SAMEORIGIN or Content-Security-Policy: frame-ancestors ''self''


7. X-Content-Type-Options is set to nosniff

Why it matters: Without this, browsers may guess (sniff) a file''s content type and execute a text file as JavaScript in a MIME confusion attack.

Fix: X-Content-Type-Options: nosniff (zero risk, add immediately)


8. Referrer-Policy is configured

Why it matters: Without this, your users'' full URLs (including query parameters that may contain tokens, IDs, or search terms) are sent to every third-party domain they navigate to.

Fix: Referrer-Policy: strict-origin-when-cross-origin


Cookies

9. All cookies have Secure, HttpOnly, and SameSite flags

Why it matters: - Without Secure: cookies can be sent over plain HTTP and intercepted - Without HttpOnly: JavaScript can read the cookie (XSS can steal session tokens) - Without SameSite: cookies are sent on cross-origin requests, enabling CSRF attacks

Check: Browser DevTools > Application > Cookies. Verify each cookie has all three flags.

Fix (Express example):

javascript
res.cookie(''session'', token, {
  httpOnly: true,
  secure: true,        // Only sent over HTTPS
  sameSite: ''strict'',  // Or ''lax'' for OAuth flows
  maxAge: 3600000,     // 1 hour
});

10. Session tokens are not exposed in URLs

Why it matters: If your session ID appears in the URL (e.g. ?sessionid=abc123), it will be logged in server logs, browser history, and sent in the Referer header to third parties.

Check: Navigate your app and confirm session tokens never appear in the URL bar.

Fix: Store sessions in HttpOnly cookies only. If you are using JWT, transmit them in the Authorization header, not in the URL.


DNS and Email Security

11. SPF record is configured

Why it matters: Without SPF, anyone can send emails appearing to come from your domain, enabling phishing attacks that impersonate your brand.

Check: dig TXT yourdomain.com | grep spf

Fix: Add a TXT record like v=spf1 include:_spf.google.com ~all (replace with your actual mail provider).


12. DMARC is in enforcement mode

Why it matters: DMARC tells receiving mail servers what to do with emails that fail SPF or DKIM checks. A p=none policy monitors but takes no action. Only p=quarantine or p=reject actually protects your users.

Check: dig TXT _dmarc.yourdomain.com

Fix:

dns
_dmarc.yourdomain.com. TXT "v=DMARC1; p=reject; rua=mailto:dmarc@yourdomain.com"

Start with p=none to monitor, then move to p=quarantine, then p=reject.


Secrets and Dependencies

13. No hardcoded secrets in source code or git history

Why it matters: Hardcoded API keys, database passwords, and private keys in your codebase are frequently discovered by automated scanners. Once committed to git, they persist even after deletion in later commits.

Check:

bash
# Install truffleHog
pip install trufflehog

# Scan your local repo
trufflehog git file://. --only-verified

Or use ZeriFlow''s Advanced Scan with a GitHub repo URL — it scans for secrets across the full commit history.

Fix: Move all secrets to environment variables. Rotate any secrets that were ever committed. Use git filter-repo to purge from history.


14. Dependencies have no known critical or high CVEs

Why it matters: Vulnerable npm packages (or Python, Ruby, PHP packages) are a common entry point for attackers. The Log4Shell and event-stream incidents showed how devastating a single compromised dependency can be.

Check:

bash
npm audit --audit-level=high

Fix: Run npm audit fix for minor updates. Review breaking changes before major version upgrades. Use Dependabot or Renovate for automated dependency updates.


Authentication and Access Control

15. Login endpoints have rate limiting

Why it matters: Without rate limiting, automated brute-force tools can try millions of password combinations against your login endpoint. Most applications offer no resistance to this by default.

Check: Try sending 20+ rapid POST requests to your login endpoint. Confirm you get rate-limited (429 response) after a threshold.

Fix (Express with express-rate-limit):

javascript
import rateLimit from ''express-rate-limit'';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10,                   // Max 10 attempts per window
  message: ''Too many login attempts. Please try again in 15 minutes.'',
  standardHeaders: true,
  legacyHeaders: false,
});

app.post(''/auth/login'', loginLimiter, loginHandler);

16. Passwords are hashed with a modern algorithm

Why it matters: Storing passwords in plain text or with MD5/SHA1 means a database breach instantly exposes all user credentials. Modern algorithms like bcrypt, Argon2, or scrypt are intentionally slow and include salting.

Check: Review your user model and auth code.

Fix:

javascript
import bcrypt from ''bcryptjs'';

// Hash on registration
const hash = await bcrypt.hash(password, 12); // 12 rounds

// Verify on login
const isValid = await bcrypt.compare(inputPassword, storedHash);

17. No sensitive data in error messages or logs

Why it matters: Stack traces, database query errors, and detailed exception messages can reveal your tech stack, table names, internal paths, and logic to an attacker. This information is used for reconnaissance.

Check: Trigger a 404, a 500, and an invalid request. Confirm error responses show generic messages, not stack traces.

Fix: Implement a global error handler that logs details internally but returns generic messages to clients:

javascript
app.use((err, req, res, next) => {
  console.error(err); // Internal logging
  res.status(500).json({ error: ''An internal error occurred.'' }); // Safe client response
});

Infrastructure and Configuration

18. Server version disclosure is disabled

Why it matters: Headers like Server: nginx/1.18.0 or X-Powered-By: Express tell attackers exactly which version of your software to look up CVEs for.

Check: curl -I https://yourdomain.com | grep -i "server\\|x-powered-by"

Fix: - Nginx: server_tokens off; - Apache: ServerTokens Prod and ServerSignature Off - Express: app.disable(''x-powered-by'') or use helmet


19. Directory listing is disabled

Why it matters: If your web server serves a directory index when no index.html is present, attackers can browse your file structure and potentially access configuration files, backups, or source code.

Check: Navigate to a directory URL on your server (e.g. https://yourdomain.com/uploads/) and confirm you get a 403 or 404, not a file listing.

Fix: - Nginx: autoindex off; (off by default, but verify) - Apache: Remove Indexes from Options in your config


20. Access logs include the information needed to investigate an incident

Why it matters: When an incident occurs, you need logs. Without IP addresses, timestamps, request paths, and user agent strings, you cannot determine what happened, when, or from where.

Check: Tail your access log and confirm it captures: timestamp, client IP, HTTP method, URL, response code, response size, and user agent.

Fix (Nginx log format example):

nginx
log_format main ''$remote_addr - $remote_user [$time_local] "$request" ''
                ''$status $body_bytes_sent "$http_referer" ''
                ''"$http_user_agent" "$http_x_forwarded_for"'';

access_log /var/log/nginx/access.log main;

Ensure logs are shipped to a centralised service (Datadog, Logtail, CloudWatch) so they persist even if your server is compromised.


Running the Full Checklist Efficiently

Manually checking all 20 items before every release is time-consuming. The good news is that about half of them can be automated:

Infrastructure checks (1–12, 18, 19) can be automated with a security scanner. ZeriFlow covers TLS, headers, cookies, DNS, email security, and server configuration in a single scan that takes under 60 seconds.

Dependency checks (14) can be automated with npm audit in CI/CD, or with Snyk/Dependabot for continuous monitoring.

Secrets scanning (13) can be automated with ZeriFlow''s Advanced Scan or truffleHog as a pre-commit hook or CI step.

Application-level checks (15–17, 20) require code review — no scanner can fully verify your business logic.

A practical approach: run an automated scan for the infrastructure items, and reserve manual review time for the application-level items that require code inspection.

The goal is not to run this checklist once at launch and forget it. Security is a continuous process — run a subset of these checks on every significant deployment, and the full list before any major release.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading