Security Practices Every Web Application Needs
Most vulnerabilities affecting small applications are well understood and preventable with a handful of consistent defaults.
By Uttam Thapa · · Security
⚡ Executive Summary (TL;DR)
Almost every vulnerability that reaches a small production application is one of a handful of well-understood mistakes: trusting a value the client sent, validating late instead of at the boundary, escaping for the wrong context, or leaking internal detail through an error message. This is the checklist I apply to every build — recalculate on the server, validate with a schema, verify webhooks against the raw body, and rate limit anything that costs money to call.
Figure 1: Security as layers. Each one assumes the layer above it has already failed.
Introduction
Security is easiest to get right when it is part of the design rather than something added before launch. Most of the vulnerabilities that affect small applications are well understood and preventable with a handful of consistent practices.
This article covers the practices I apply to every production application, and the reasoning behind each one.
Never Trust the Client
This is the principle everything else follows from. Anything sent from a browser can be modified, replayed or forged.
What This Means in Practice
- Validate every request body on the server, even if the form already validates.
- Never trust a price, total or quantity that came from the client.
- Never rely on a hidden field or a disabled input to enforce anything.
- Recalculate anything that matters from data you control.
A checkout flow that accepts the total from the client is the classic example. The amount charged should always be computed on the server from the cart contents.
Validate at the Boundary
Validation belongs at the point where untrusted data enters the system, using a schema rather than scattered checks.
Request -> schema validation -> typed, trusted data -> business logic
The benefits compound.
- Unknown fields are stripped rather than passed through.
- Types are guaranteed after the boundary.
- Error messages are consistent.
- Business logic never has to defend itself.
Length limits matter here too. A message field with no maximum is an easy way to fill a database or exhaust memory.
Secrets and Configuration
Credentials in a repository are one of the most common and most damaging mistakes.
Rules
- Every environment file is ignored by version control.
- An example file with placeholder values is committed instead.
- Secrets live in the hosting platform's environment settings.
- Configuration is validated at startup so a missing value fails immediately.
One detail that is easy to miss: deleting a secret from a file does not remove it from version control history. If a credential has ever been committed, it must be rotated, not just deleted.
Preventing Cross-Site Scripting
XSS happens when untrusted input is rendered as markup.
- Escape user input before interpolating it into HTML.
- Be careful with any API that renders raw HTML.
- Escape for the context you are inserting into.
A Subtle Mistake
Escaping is for HTML output only. Applying HTML escaping to an email header, a URL parameter or a database value corrupts the data. An email address escaped as HTML will have an ampersand turned into an entity, and the reply address will no longer work.
Escape at the point of rendering, not at the point of storage.
Rate Limiting
Any public endpoint that sends an email, writes to a database or calls a paid API needs a rate limit.
- Contact forms, to prevent spam relaying.
- Newsletter signups, to prevent list poisoning.
- Authentication endpoints, to slow credential stuffing.
When the application runs behind a proxy, the framework must be configured to trust it. Otherwise every request appears to come from the load balancer address and the rate limit applies to all users collectively.
Webhook Verification
A webhook endpoint is a public URL that performs privileged actions. It must verify that the request genuinely came from the provider.
Receive raw body
-> compute HMAC signature
-> compare against the header
-> only then act on it
The Common Failure
Body-parsing middleware often consumes and reformats the request body before verification runs. The signature is computed over the original bytes, so any reformatting invalidates it. The webhook route needs the raw body specifically.
This usually presents as verification failing intermittently, which makes it look like a provider problem rather than a middleware ordering problem.
Error Messages
Error handling is a security surface.
- Operational errors, such as validation failures, can be shown to the user.
- Unexpected errors should be logged in full and returned as a generic message.
- Stack traces and database messages should never reach the client in production.
A single error handler that distinguishes between these two categories is far safer than handling errors individually in each route.
CORS
Cross-origin rules should be an explicit allowlist, not a wildcard.
One practical detail: origins are compared as exact strings, and browsers send the origin without a trailing slash. A configured value with a trailing slash will silently fail to match, which usually shows up as requests working locally and failing in production.
Dependencies
- Audit dependencies regularly and read what the advisories actually say.
- Distinguish between a vulnerability in a runtime dependency and one in a build tool.
- Remove packages that are no longer used, rather than leaving them installed.
- Prefer maintained packages; an unmaintained one accumulates advisories that can never be fixed.
Key Takeaways
- ✓Recalculate anything that matters on the server.
- ✓Validate with a schema at the boundary.
- ✓Escape for HTML output only, never for headers or storage.
- ✓Rate limit every endpoint that costs something to call.
- ✓Verify webhook signatures against the raw body.
- ✓Never return internal error detail to the client.
- ✓Rotate any credential that has ever been committed.
None of these are advanced techniques. They are defaults, and applying them consistently prevents the large majority of issues that affect applications at this scale.
Frequently asked questions
What are the most important security practices for a small web application?
Recalculate anything that matters on the server, validate input with a schema at the boundary, escape output for the context it lands in, rate limit endpoints that cost money to call, and never return internal error detail to the client.
Is client-side validation enough?
No. Client-side validation is a usability feature that improves the experience for cooperative users. Every request must be validated again on the server, because the client is under the caller's control.
How do you verify a webhook is genuine?
Compute the signature over the raw, unparsed request body using the shared secret and compare it in constant time. Parsing the body first changes the bytes and the signature will never match.
Home · Projects · Blog · Services · Résumé · Contact