Developer security guide

How to secure a web application

Build security around trust boundaries: authenticate users safely, authorize every sensitive action and object, constrain untrusted input, protect browser flows, and verify the controls with negative tests before release.

Short answer: Build security around trust boundaries: authenticate users safely, authorize every sensitive action and object, constrain untrusted input, protect browser flows, and verify the controls with negative tests before release.

1. Map assets and trust boundaries

Start with the flows that move money, expose private data, change privileges or call another system. Draw the transitions between an anonymous browser, an authenticated session, your API, background workers, storage and third-party services. For each transition, record the identity used and the authorization decision expected.

Turn that map into abuse cases: a member reads another workspace, an attacker makes the image proxy call a private address, or a forged webhook changes a subscription. This produces testable work instead of a generic list of controls.

2. Separate authentication, session and authorization

Authentication establishes who is acting; the session carries that identity; authorization decides whether this identity may perform this action on this object. Keep those decisions server-side. Use established authentication libraries, rotate the session identifier after login or privilege changes, set cookies with HttpOnly, Secure and an appropriate SameSite value, and require re-authentication for high-impact changes.

Authorization must run on every request, including API routes, export jobs, static private files and background actions. Prefer deny-by-default policies and query data inside the current tenant rather than loading by a global identifier and checking later.

3. Constrain input at the point of use

Validate syntax and business constraints on the server: types, lengths, allowed values, state transitions and ownership. Validation is not the same as output encoding. Parameterize database queries, encode untrusted text for the destination context, and avoid passing user values to shells, template evaluators or dynamic module loaders.

For outbound URLs, allow known schemes and destinations, resolve and check addresses, reject loopback, private and link-local ranges, limit redirects, and apply network egress controls. URL parsing alone does not stop SSRF.

const project = await db.project.findFirst({
  where: { id: input.projectId, workspaceId: session.workspaceId }
});
if (!project || !session.permissions.includes("project:update")) {
  return response(404);
}
await updateProject(project.id, validatedPatch);

4. Protect browser-facing flows

Use HTTPS everywhere and send HSTS only from an HTTPS deployment. Add a tested Content Security Policy, frame restrictions, X-Content-Type-Options: nosniff, a conservative referrer policy and a permissions policy. State-changing cookie-authenticated requests need CSRF protection; SameSite helps but is not a complete design by itself.

Uploads need independent checks for size, expected type, generated storage names and safe delivery. Store them outside executable paths and serve user content from a deliberately constrained origin when possible.

5. Verify controls before release

Add negative tests for unauthenticated users, the wrong role, the wrong tenant, guessed identifiers and stale sessions. Exercise rate limits and recovery paths. Run dependency and secret checks continuously, then use focused manual review for business rules and multi-step abuse that tools cannot infer from one file.

Common mistakes

  • Hiding a button but leaving its API action unprotected.
  • Checking a role without checking ownership or tenant membership.
  • Accepting any https URL in a server-side fetcher.
  • Escaping input once and reusing it in HTML, SQL and URLs.
  • Logging tokens, password-reset links or full sensitive request bodies.
  • Shipping a CSP copied from another app without testing actual resources.

Release checklist

  • Critical assets and trust transitions are documented.
  • Every sensitive route and object has a server-side authorization test.
  • Sessions use secure cookie settings and rotate at privilege changes.
  • Database queries are parameterized; output is encoded for its context.
  • Outbound requests have destination and egress restrictions.
  • CSRF, clickjacking and content-type controls are tested.
  • Secrets are outside source control and have a rotation procedure.
  • Negative integration tests run in CI.

Limits

A checklist cannot model every product rule, production configuration or third-party dependency. Static review also cannot prove how a deployed system behaves. Combine secure design, automated checks, manual code review and an authorized runtime test appropriate to the application's risk.

Primary sources

Review a risky release

Apply these controls to the actual code and product boundaries.

Discuss a code review