HomeProjectsBlogContact

Web Application Security Checklist for Full Stack Developers

You don't need to be a security specialist to avoid the bugs attackers exploit most. A practical checklist covering injection, XSS, CSRF, access control, secrets, dependencies and headers.

Web Application Security Checklist for Full Stack Developers

Most web apps don't get breached through some brilliant zero-day. They get breached through the same handful of mistakes, over and over: a query built with string concatenation, an endpoint that forgets to check who owns a record, a secret sitting in a public repo. The good news is that these are all preventable with habits, not heroics.

Use this as a checklist before you ship, and again whenever you add a major feature. It lines up with the categories in the OWASP Top 10, which is worth reading in full once.

1. Injection: never build queries from strings

// Vulnerable
db.query(`SELECT * FROM users WHERE email = '${email}'`)

// Safe: parameterized
db.query('SELECT * FROM users WHERE email = $1', [email])
  • Every SQL query uses parameters or a query builder/ORM that parameterizes for you.
  • Raw SQL escape hatches in your ORM (raw, unsafe, literal) are reviewed carefully.
  • No user input is passed to shell commands, eval, or template engines without strict validation.

2. Cross-site scripting (XSS)

XSS happens when user-supplied content runs as code in someone else's browser.

  • Output is escaped by default. React, Vue, Svelte and most template engines do this, so don't bypass it casually.
  • Any use of dangerouslySetInnerHTML, v-html or innerHTML with user content goes through a sanitizer like DOMPurify.
  • Markdown rendered from user input is sanitized after conversion to HTML.
  • A Content Security Policy restricts where scripts can load from. Start in report-only mode, then enforce.

3. Broken access control (the most common serious bug)

Authentication answers "who are you?" Authorization answers "are you allowed to do this, to this record?" Forgetting the second one is extremely common.

// Vulnerable: any logged-in user can read any invoice by changing the ID
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
  res.json(await Invoice.findByPk(req.params.id))
})

// Safe: scope the query to the current user
const invoice = await Invoice.findOne({ where: { id: req.params.id, userId: req.user.id } })
if (!invoice) return res.status(404).end()
  • Every endpoint that reads or modifies a record checks ownership or permissions on the server.
  • Hiding a button in the UI is never treated as security.
  • Admin routes check roles server-side, on every request.
  • If you query a database from the frontend (Supabase, Firebase), row-level security or security rules are enabled and tested.

4. Authentication and passwords

  • Passwords are hashed with a slow, salted algorithm built for passwords: Argon2id, bcrypt or scrypt. Never MD5, SHA-1 or plain SHA-256.
  • Login, signup and password reset are rate limited.
  • Password reset tokens are random, single-use and expire quickly.
  • Login errors don't reveal whether an email exists ("Invalid email or password").
  • Session cookies are HttpOnly, Secure and SameSite. (Sessions vs JWT, compared.)
  • Multi-factor authentication is available, at least for admin accounts.

If you can, use a well-maintained auth library or provider instead of writing this yourself.

5. Cross-site request forgery (CSRF)

If you authenticate with cookies, another site can try to make the user's browser send requests to yours.

  • Cookies use SameSite=Lax or Strict.
  • State-changing actions use POST/PUT/PATCH/DELETE, never GET.
  • Your framework's CSRF protection is enabled for cookie-authenticated forms.

6. Secrets and configuration

  • No secrets in the codebase or Git history. (Environment variables done right.)
  • Browser-exposed variables contain nothing sensitive.
  • Production, staging and development use different credentials.
  • Debug mode is off in production, and error responses don't include stack traces.
  • Secret scanning is turned on for your repositories. (What to do if a key leaks.)

7. Dependencies

Your app is mostly other people's code.

  • Automated dependency updates are enabled (Dependabot, Renovate).
  • npm audit, pip-audit or equivalent runs in CI.
  • Lockfiles are committed so installs are reproducible.
  • New packages get a quick sanity check: maintained? widely used? spelled correctly? (Typosquatted packages are a real attack.)

8. Transport and security headers

  • HTTPS everywhere, with HTTP redirected to HTTPS.
  • Strict-Transport-Security (HSTS) is set.
  • X-Content-Type-Options: nosniff is set.
  • Framing is restricted with CSP frame-ancestors (or X-Frame-Options) to prevent clickjacking.
  • Referrer-Policy is set to something sensible, like strict-origin-when-cross-origin.

Libraries like Helmet (Express) set most of these in one line.

9. Input validation and file uploads

  • Every API input is validated on the server for type, length and format, using a schema library.
  • Request body size is limited.
  • File uploads check real file type, enforce size limits and are stored outside the web root. (Full guide.)

10. Logging and monitoring

  • Failed logins, permission denials and unusual errors are logged.
  • Logs never contain passwords, tokens or full card numbers.
  • You get alerted on error spikes, not just when a user emails you.
  • Backups exist, are automated and have been restored at least once.

Make it routine

Security isn't a phase at the end of a project. A few habits keep you ahead:

  • Add "who is allowed to do this?" to your mental checklist for every new endpoint.
  • Write tests for authorization: user A tries to read user B's data and must get a 403 or 404.
  • Re-run this checklist before every major release.

FAQ

What is the most common web application vulnerability?

Broken access control, meaning users can reach data or actions they shouldn't, consistently ranks at the top. It's usually a missing ownership check on the server.

Does using a framework make my app secure?

Frameworks prevent many issues by default (escaping output, parameterized queries, CSRF tokens), but they can't check your authorization logic or stop you from bypassing their protections.

Which password hashing algorithm should I use?

Argon2id is the modern recommendation, with bcrypt and scrypt as solid alternatives. Use your language's well-maintained library rather than implementing anything yourself.

Keep reading

More from the workbench

All articles →