SaaS Application Security Checklist: Identity, Tenant Isolation, and Defense in Depth
In multi-tenant software-as-a-service (SaaS), security is not a feature added just before launch. It is an architectural responsibility that shapes how customer data is accessed, processed, stored, and protected.
When businesses adopt a SaaS platform, they entrust it with customer records, intellectual property, operational data, and sometimes financial information. A flaw in authorization logic, tenant scoping, or session management can expose data belonging to another organization, turning a single vulnerability into a serious incident.
Securing a multi-tenant application requires defense in depth: multiple independent safeguards that reduce the likelihood of a breach and limit its impact if one control fails.
This guide presents a practical security checklist for SaaS engineering teams, covering identity and access management, tenant isolation, API authorization, input validation, cryptography, audit logging, and deployment safeguards.
1. Identity, Authentication, and Access Governance
Authentication establishes who a user is. Authorization determines what that user can do. A secure SaaS application needs both, with permissions enforced consistently on the server.
Strengthen authentication
Support multi-factor authentication (MFA): Offer authenticator-app TOTP and phishing-resistant passkeys or security keys using WebAuthn/FIDO2. Require stronger authentication for privileged accounts and sensitive operations.
Provide organization-level policies: Where the product supports organizational administration, allow appropriate administrators to require MFA for workspace members.
Consider enterprise single sign-on (SSO): Support SAML 2.0 or OpenID Connect (OIDC) when enterprise customer requirements justify it.
Automate account lifecycle management: For enterprise deployments, SCIM provisioning and deprovisioning can help synchronize user accounts with customer identity systems. Treat role assignments and deprovisioning as security-sensitive operations.
SMS-based authentication can be useful as a fallback in some contexts, but it is more vulnerable to attacks such as SIM swapping than phishing-resistant authentication methods.
Secure sessions and tokens
For browser-based applications, consider server-managed sessions using cookies configured with appropriate attributes:
Set-Cookie: __Host-session=<opaque-session-id>; Path=/; Secure; HttpOnly; SameSite=LaxThe __Host- prefix requires a secure cookie without a Domain attribute and with Path=/, helping restrict its scope.
Important safeguards include:
Use
Secureto restrict cookie transmission to HTTPS.Use
HttpOnlyto prevent client-side JavaScript from directly reading the cookie.Choose
SameSite=LaxorSameSite=Strictaccording to the application's navigation and authentication requirements.Implement CSRF protections where required by the authentication architecture.
SameSiteis useful defense in depth, not a universal substitute for CSRF defenses.Enforce session expiration and revocation on the server.
Rotate sessions after authentication and relevant privilege changes.
Invalidate sessions after logout, password changes, account recovery, or security incidents as appropriate.
Require reauthentication for particularly sensitive operations.
Avoid storing long-lived authentication credentials in localStorage, where injected JavaScript can read them. An HttpOnly cookie helps protect token confidentiality, but it does not make an application immune to cross-site scripting (XSS). An XSS payload may still issue authenticated requests from the compromised browser.
Verification: Test logout, session expiration, password resets, privilege changes, and CSRF protections. Confirm that a revoked session cannot continue accessing protected resources.
For additional guidance, consult the OWASP Session Management Cheat Sheet.
2. Enforce Tenant Isolation and Object-Level Authorization
One of the most serious multi-tenant SaaS risks is Broken Object Level Authorization (BOLA), also known as an insecure direct object reference (IDOR).
BOLA occurs when an application accepts a resource identifier but fails to verify that the authenticated user is authorized to access that specific resource.
Imagine an authenticated user requesting:
GET /api/invoices/inv_99812If the invoice belongs to another organization and the application retrieves it using only its ID, the endpoint may expose another customer's data.
The problem is not that the identifier is guessable. The problem is that the server fails to enforce authorization.
Bind resource access to the authenticated tenant
For example, a tenant-scoped query might look like this:
SELECT id, invoice_number, total
FROM invoices
WHERE id = $1
AND organization_id = $2;The parameters must come from validated request input and trusted server-side authorization context, respectively. Do not accept a client-supplied organization ID as proof that the user belongs to that organization.
Apply the same principle to reads, updates, deletes, exports, file downloads, and background jobs.
For updates, ensure that both the target row and any resulting tenant ownership are authorized. A user must not be able to change an object's organization identifier through mass assignment or an improperly scoped update.
Use PostgreSQL Row-Level Security where appropriate
PostgreSQL Row-Level Security (RLS) can provide an additional database-enforced tenant boundary.
A simplified example follows:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY invoices_tenant_isolation
ON invoices
FOR ALL
USING (
organization_id =
NULLIF(
current_setting('app.current_tenant_id', true),
''
)::uuid
)
WITH CHECK (
organization_id =
NULLIF(
current_setting('app.current_tenant_id', true),
''
)::uuid
);This example assumes that organization_id is a UUID and that the application sets app.current_tenant_id for each transaction.
Set the tenant context transactionally:
BEGIN;
SELECT set_config(
'app.current_tenant_id',
$1,
true
);
SELECT id, invoice_number, total
FROM invoices;
COMMIT;The third argument to set_config makes the setting transaction-local. In a pooled database connection, this helps prevent tenant context from unintentionally persisting across transactions.
However, a custom session setting is not an independent identity-verification mechanism. Application code must establish the correct tenant context, and the database role must not be able to bypass the intended security boundary.
For a production implementation:
Use a least-privilege database role that does not own the protected tables and does not have
BYPASSRLS.Understand the behavior of superusers, table owners, and
FORCE ROW LEVEL SECURITY.Apply and test policies across all relevant tenant-owned tables and operations.
Use transaction-safe tenant context management with the actual connection pool.
Test missing, invalid, and incorrectly scoped tenant context.
Keep application-level membership and permission checks in place.
RLS is an additional safeguard, not a replacement for sound authorization architecture. PostgreSQL documents the behavior and limitations in its Row Security Policies documentation.
Do not rely on opaque identifiers alone
UUIDs and other high-entropy identifiers can make resource enumeration harder, but they do not prevent BOLA. An attacker who obtains another resource's identifier may still exploit an endpoint that lacks authorization checks.
Use unpredictable identifiers where appropriate, but always enforce resource ownership and permissions on the server.
Verification: Create at least two test organizations and attempt cross-tenant reads, updates, deletes, exports, and file downloads using each organization's authenticated users. Every unauthorized operation must fail without exposing protected data.
For a broader API threat model, review the OWASP API Security Top 10.
3. Validate Inputs and Prevent Injection
Treat incoming network data as untrusted, regardless of whether it comes from an authenticated user.
Validation should happen at API boundaries, before input is used in business logic or persistence operations.
Validate request schemas
Use schema validation libraries such as Zod or JSON Schema to validate:
Request bodies
Query parameters and path identifiers
Pagination and sorting options
Webhook payloads
File metadata
Configuration and environment variables
For example, a Zod schema might define a constrained invoice update:
import { z } from "zod";
const UpdateInvoiceSchema = z.object({
dueDate: z.string().date().optional(),
notes: z.string().max(2000).optional()
}).strict();
type UpdateInvoiceInput =
z.infer<typeof UpdateInvoiceSchema>;The .strict() option rejects unknown object keys rather than silently accepting them. Other schemas may intentionally strip unknown keys instead. Choose a consistent policy based on the endpoint's contract.
Notice that the schema does not accept organizationId. Tenant ownership should be established through trusted server-side context, not arbitrary client input.
Reject invalid requests with an appropriate 4xx response, commonly 400 Bad Request or 422 Unprocessable Content, according to the API's documented conventions.
Prevent SQL injection
Use parameterized queries, prepared statements, or correctly implemented ORM query methods.
Avoid constructing SQL statements by concatenating untrusted input. Where users can choose sorting or filtering fields, validate those choices against an explicit allowlist because database identifiers generally cannot be supplied as ordinary query parameters.
An ORM reduces some common risks, but unsafe raw queries can reintroduce injection vulnerabilities.
Defend against cross-site scripting
When rendering user-generated rich text, sanitize HTML using a maintained library such as DOMPurify, configured for the intended content.
Also:
Prefer context-appropriate output encoding.
Avoid inserting untrusted content into executable JavaScript or unsafe HTML contexts.
Set a Content Security Policy (CSP) suited to the actual application.
Avoid relying on CSP as a replacement for output encoding and sanitization.
A basic CSP starting point is:
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'This is an illustrative baseline, not a complete drop-in policy for every application. Real applications may need carefully restricted script-src, connect-src, img-src, and other directives. Test the policy against legitimate functionality before enforcing it in production.
Verification: Test malformed payloads, unexpected object properties, injection attempts, and malicious rich-text input. Confirm that rejected data does not reach sensitive business operations.
4. Protect Passwords, Encryption, and Secrets
Security-sensitive data needs different controls depending on whether it is a password, an application secret, or stored business information.
Hash passwords using suitable algorithms
Never store plaintext passwords or protect them using fast general-purpose hashes such as MD5, SHA-1, or unsalted SHA-256.
Use a modern password-hashing algorithm such as Argon2id. If it is unavailable, choose a suitable alternative such as scrypt or bcrypt, with parameters appropriate to the implementation and current guidance.
Password-hashing parameters should be benchmarked on the target infrastructure. They must make offline guessing expensive while keeping legitimate authentication practical.
If a managed identity provider handles authentication, follow its documented security configuration rather than implementing a separate password system unnecessarily.
See the OWASP Password Storage Cheat Sheet.
Encrypt data in transit
Use HTTPS with modern TLS configurations. TLS 1.3 is a strong default where supported, while securely configured TLS 1.2 may be required for compatibility.
Redirect HTTP traffic to HTTPS and configure HTTP Strict Transport Security (HSTS) after confirming that the domain and its subdomains are ready for the policy.
For example:
Strict-Transport-Security: max-age=31536000This is an illustrative one-year policy. Add includeSubDomains only when all relevant subdomains can safely support HTTPS. Do not enable HSTS preloading without understanding the associated domain-wide implications.
Encrypt stored data appropriately
Use encryption at rest for databases, backups, and object storage, using suitable platform-managed encryption or customer-managed keys where the threat model and customer requirements justify them.
Encryption does not replace access controls. Restrict access to keys, separate duties where appropriate, and define how keys are rotated and recovered.
Keep secrets out of source control
Never commit credentials, API keys, private keys, or production tokens to Git.
Use secret managers or the deployment platform's protected environment-variable facilities.
Run automated secret scanning in local workflows and CI/CD.
Rotate exposed credentials immediately and investigate their potential use.
Apply least-privilege permissions to service accounts and deployment identities.
Removing a secret from the latest commit does not invalidate a credential that has already been exposed. Rotate it and assess the relevant Git history, logs, and deployment environments.
5. Build Useful Audit Trails and Incident Readiness
No security control guarantees that incidents will never happen. A mature SaaS platform needs to detect suspicious activity, investigate events, and respond effectively.
Record security-relevant events
An audit trail should capture meaningful actions such as:
Authentication successes and failures
MFA and account recovery changes
Role and permission updates
Organization membership changes
Sensitive data exports
Administrative configuration changes
Security-policy modifications
An example event might look like this:
{
"timestamp": "2026-10-22T14:30:00Z",
"actorId": "usr_91823",
"tenantId": "org_48201",
"action": "member.role.updated",
"targetId": "usr_10294",
"metadata": {
"previousRole": "MEMBER",
"newRole": "ADMIN"
}
}The identifiers and values above are illustrative.
Capture enough context to investigate an event while minimizing unnecessary personal information. Protect logs from unauthorized access, alteration, and deletion. For higher-assurance environments, consider append-only storage, restricted deletion permissions, tamper-evident mechanisms, and retention policies.
Avoid logging passwords, access tokens, session identifiers, or sensitive customer payloads.
Prepare for incident response
Define how the team will detect, investigate, contain, and recover from a security incident.
Useful controls include:
Alerts for suspicious authentication and authorization patterns
Documented credential and key rotation procedures
Clear incident ownership and escalation paths
Protected, searchable security logs
Regular dependency and infrastructure reviews
Tested backup restoration procedures
Documented customer communication and notification responsibilities
Test backups and disaster recovery
A backup that has never been restored is an unverified recovery assumption.
Maintain protected backups and perform restoration exercises. Measure whether recovery procedures meet the system's Recovery Time Objective (RTO) and Recovery Point Objective (RPO).
Verification: Confirm that a backup can actually be restored, that recovered data is consistent, and that the team can execute the recovery procedure within its documented objectives.
6. Prioritize the Security Work
Not every control has the same urgency for every product. The following checklist uses suggested priorities that should be adjusted to the application's architecture, data sensitivity, exposure, and threat model.
PriorityAreaActionVerificationP0Tenant isolationEnforce tenant-scoped authorization on every resource operationAutomated cross-tenant testsP0Access controlVerify server-side permissions for users, roles, and sensitive actionsNegative authorization testsP0AuthenticationSecure session handling, expiration, and revocationSession lifecycle testsP0InjectionUse parameterized queries and validate untrusted inputSecurity tests and code analysisP1SecretsScan repositories and CI/CD for exposed credentialsAutomated secret scanningP1DependenciesMonitor vulnerable packages and prioritize remediationDependency scanning and patch reviewP1NetworkEnforce HTTPS and review TLS and security headersExternal configuration checksP1API abuseRate-limit sensitive endpoints and resource-intensive operationsRate-limit and load testsP1RecoveryMaintain backups and verify restorationScheduled recovery drillsP2AuditabilityRecord and protect security-relevant eventsAudit trail reviewP2MonitoringAlert on suspicious access and operational anomaliesIncident-response exercises
These priority labels are a practical planning aid, not a universal security standard. For an application with a known critical weakness, remediation should take precedence over any generic ordering.
7. Make Security Part of the Development Lifecycle
Security should be integrated into development and deployment rather than postponed until a release is imminent.
For each significant release, teams should consider the following:
Code review: Review authorization, tenant context, unsafe queries, and sensitive-data handling.
Automated testing: Include negative authorization tests, malformed input cases, and tenant-isolation regressions.
CI/CD controls: Run type checks, linting, tests, dependency scans, and secret scans as appropriate for the project.
Least privilege: Restrict database roles, service identities, CI/CD permissions, and production access.
Safe deployment: Review migrations, configuration changes, and security-sensitive defaults before rollout.
Ongoing maintenance: Patch dependencies, rotate credentials when necessary, review access, and respond to security advisories.
Automated scanners are valuable, but a passing scan is not proof that a system is secure. They must complement manual review, threat modeling, and tests that reflect the application's actual authorization rules.
Conclusion
Securing a multi-tenant SaaS application requires more than enabling HTTPS or adding an authentication library. It requires enforcing identity and permissions, protecting tenant boundaries, validating untrusted input, managing secrets responsibly, and preparing for incidents.
The most important principle is to verify security boundaries rather than assume they work. Tenant isolation should be tested across real application operations. Sessions should be tested for expiration and revocation. Backups should be restored. Security events should be traceable.
At Aformix, we build our own software products and also take on client work: custom web development, WordPress development, SEO, and software and SaaS engineering. Secure architecture, clear authorization boundaries, and disciplined engineering practices are important considerations when building software that businesses can depend on.
Disclaimer: This article provides general software engineering guidance, not a certification of security or regulatory compliance. Requirements for frameworks and standards such as SOC 2, ISO 27001, or HIPAA depend on the organization's circumstances and applicable obligations. Formal compliance may require documented controls, independent assessments, legal guidance, and ongoing operational evidence.

