Web Security Essentials Every SaaS Team Must Implement
The OWASP-aligned checklist covering authentication, session handling, and input validation that every SaaS product should implement before launch.
Most SaaS security incidents are not sophisticated. They are an endpoint that checked whether you were logged in but not whether the record belonged to you, a dependency that had a published fix six months earlier, or an administrative interface that was never meant to be reachable from the internet.
This is the set of controls that prevents the incidents that actually happen, roughly in order of how often they are the cause.
Broken Access Control Is the Number One Cause
Authentication asks who you are. Authorization asks what you may do. Products get the first right and the second wrong constantly, because authentication is centralized in one place and authorization is scattered across every endpoint.
The archetypal bug: an endpoint that accepts a record identifier, confirms the caller is logged in, and returns the record without checking who owns it. Changing the identifier in the URL returns someone else's data. This is trivially discoverable and it is the most common serious vulnerability in SaaS applications.
Every request that touches a specific resource must verify that the authenticated subject is permitted to act on that specific resource. Not that they are logged in. Not that they have a role. That they may perform this action on this object.
Make it structural rather than remembered. A single authorization function called from every handler is auditable; checks inlined in route handlers are not, and the one that was forgotten is invisible until someone finds it.
Where the database supports it, enforce tenant scoping at the data layer with row-level security so that a query missing its filter returns nothing rather than everything. Failing closed instead of open is the entire value of that control.
Do not rely on unguessable identifiers as a security measure. Random identifiers are good practice because sequential ones leak volume and enable enumeration, but they are a defense in depth measure, not an access control.
Get Session Handling Right
Session management is where a lot of subtle problems live.
Store session tokens in cookies marked HttpOnly, Secure, and SameSite. HttpOnly keeps JavaScript from reading them, which limits the damage of a cross-site scripting flaw. Secure keeps them off plaintext connections. SameSite blocks the cross-site request forgery class almost entirely.
Storing tokens in local storage is common and worse: any script running on the page can read it, including a compromised dependency. The convenience is not worth the exposure.
Regenerate the session identifier on login and on any privilege change. Failing to do so allows session fixation, where an attacker sets a known session identifier before the victim authenticates and inherits the authenticated session.
Invalidate sessions server-side on logout. A token that remains valid after logout is still valid to whoever has a copy of it. This is why purely stateless tokens with no revocation mechanism are a poor fit for session management — keep access tokens short-lived and maintain revocable refresh tokens server-side.
Expire idle sessions and enforce an absolute maximum lifetime regardless of activity.
Authentication Details That Matter
Hash passwords with a memory-hard algorithm designed for the purpose — argon2 or bcrypt. General-purpose hash functions are fast, and fast is exactly wrong here.
Set a minimum length rather than composition rules. Long passphrases are stronger than short strings with a symbol requirement, and composition rules mainly produce predictable substitutions. Check candidate passwords against a list of known breached passwords, which catches far more real risk than complexity rules.
Rate limit authentication by account and by source address, with progressive delays. Credential stuffing attacks use valid passwords from other breaches, so the defense is limiting attempt volume rather than password strength.
Offer multi-factor authentication and require it for administrative roles. For a product with business customers, this is also a procurement requirement you will meet eventually.
Make password reset flows single-use, short-lived, and constant-response — the reset endpoint should respond identically whether or not the account exists, or it becomes an account enumeration tool. The same applies to login errors and signup.
Validate Input at the Boundary, Encode at Output
Two separate controls that get conflated.
Validate every input against a schema at the point it enters the system: type, length, format, allowed values. Reject what does not conform rather than trying to sanitize it into shape. Define the schema once and derive your types from it so validation and types cannot drift apart.
Validate on the server always. Client-side validation is a user experience feature and provides no security, since the client is under the attacker's control.
Then encode at output, contextually. Injection happens when data is interpreted as code, and the correct encoding depends entirely on where the value lands. Parameterized queries for SQL — never string concatenation, and never for the ORM escape hatch either. Framework-managed escaping for HTML, with any raw-HTML insertion path treated as a reviewed exception. Careful handling of anything that reaches a shell command, a file path, or a template engine.
For rich text that must render as HTML, sanitize with a well-maintained library and a strict allowlist. Writing your own sanitizer is a reliable way to produce a vulnerability.
Server-Side Request Forgery Deserves Specific Attention
Any feature that fetches a user-supplied URL — webhook configuration, image import, link previews, document fetching — can be pointed at your internal network or at cloud metadata endpoints that return credentials.
Blocklists do not work; there are too many encodings and redirect tricks. Use an allowlist of permitted destinations where the feature allows it. Otherwise, resolve the hostname and reject private and loopback address ranges before connecting, re-validate after every redirect, and make the outbound request from a network segment that cannot reach internal services.
Manage Dependencies as Ongoing Work
The majority of vulnerable code in a typical application was not written by the team that ships it. Vulnerabilities in dependencies are published with fixes available, and the exposure window is entirely a function of how quickly you update.
Automate scanning in the pipeline and automate update pull requests so upgrades are small and routine rather than a periodic large project. Commit the lock file so builds are reproducible. Audit what you are actually pulling in — a package with sixty transitive dependencies for one small utility is a large surface for a small benefit.
Set the Security Headers
A handful of response headers eliminate whole categories of attack and take an afternoon to configure.
A content security policy is the most valuable and the most work, because it requires knowing what your pages legitimately load. Start in report-only mode, collect violations, then enforce. It is the strongest available mitigation for cross-site scripting.
Strict transport security forces HTTPS for future requests. Frame options or the equivalent policy directive prevents clickjacking. Content type options stops browsers from guessing content types in ways that turn an uploaded file into executable script. Referrer policy stops URLs containing tokens from leaking to third parties.
Do Not Log Secrets, and Do Log Security Events
Credentials, tokens, card numbers, and full request bodies from authentication endpoints should never reach your logs, where they persist far longer than anywhere else and are visible to anyone with log access. Redact at the logging layer rather than trusting every call site.
Conversely, log the events that matter for investigating an incident: authentications and failures, privilege changes, permission denials, password and email changes, data exports, and administrative actions. Include actor, target, source address, and timestamp.
Alert on the patterns that indicate an attack in progress — a spike in permission denials from one account, authentication failures across many accounts, an unusual export volume. Logs nobody looks at are only useful after the fact.
File Uploads Are an Execution Risk
Validate the file type by inspecting content rather than trusting the extension or the declared content type, both of which are attacker-controlled. Enforce size limits. Generate your own filename rather than using the supplied one, which prevents path traversal and overwriting.
Store uploads outside the web root, ideally in object storage on a different domain, and serve them with a content type that will not execute and a disposition header that forces download for anything not explicitly intended to render.
Before You Launch
The realistic minimum: every resource-touching endpoint checks ownership, sessions are cookie-based with the right flags and are invalidated on logout, passwords are hashed with a memory-hard algorithm and authentication is rate limited, input is schema-validated server-side, queries are parameterized, security headers are set, dependency scanning runs in the pipeline, secrets are out of the repository and out of the logs, and security events are logged and alerted on.
None of this is advanced. It is the list of things that, when missing, account for nearly every breach that gets written up — and the failures are rarely clever. They are ordinary controls that nobody owned.