SaaS Security Checklist for Startups: What to Lock Down First

Published 9 min read
saas securitystartup securityauthenticationsecrets managementincident response

Startup security is not about achieving enterprise-grade compliance from day one. It is about locking down the controls that prevent the most damaging attacks first, then layering additional protections as the product and team grow. This checklist covers authentication with OAuth and JWT, authorization with RBAC, input validation, SQL injection prevention, XSS protection, secrets management, dependency scanning, and incident response -- prioritized by risk and effort so a 2-5 person engineering team can implement them in order.

Authentication: OAuth, JWT, and Session Security

Authentication is the front door. If it is weak, nothing else matters.

OAuth 2.0 Implementation

For customer-facing login, use OAuth 2.0 with a proven identity provider rather than building your own:

// OAuth 2.0 Authorization Code flow with PKCE (recommended for SPAs)

const authConfig = {

authority: "https://auth.example.com",

client_id: "your-app-client-id",

redirect_uri: "https://app.example.com/callback",

response_type: "code",

scope: "openid profile email",

code_challenge_method: "S256",

};

// Generate PKCE challenge

function generateCodeVerifier() {

const array = new Uint8Array(32);

crypto.getRandomValues(array);

return btoa(String.fromCharCode(...array))

.replace(/\+/g, "-")

.replace(/\//g, "_")

.replace(/=+$/, "");

}

OAuth checklist:

  • Always use Authorization Code flow with PKCE for browser apps (never Implicit flow).
  • Validate the `state` parameter to prevent CSRF.
  • Store tokens securely: HTTP-only cookies for web, secure storage for mobile.
  • Implement token refresh with rotation (invalidate old refresh tokens).

JWT Best Practices

If you issue your own JWTs for API authentication:

<h1>Python JWT creation with proper claims</h1>

import jwt

from datetime import datetime, timedelta

def create_access_token(user_id: int, role: str) -> str:

payload = {

"sub": str(user_id),

"role": role,

"iat": datetime.utcnow(),

"exp": datetime.utcnow() + timedelta(hours=1), # Short-lived

"iss": "https://api.example.com",

"aud": "https://app.example.com",

}

return jwt.encode(payload, JWT_SECRET, algorithm="HS256")

def verify_token(token: str) -> dict:

return jwt.decode(

token,

JWT_SECRET,

algorithms=["HS256"], # Explicit algorithm - prevents alg:none attack

audience="https://app.example.com",

issuer="https://api.example.com",

)

JWT security rules:

RuleWhy

Set `exp` to 1 hour or lessLimits damage from stolen tokens
Always specify `algorithms` in verifyPrevents algorithm confusion attacks
Include `iss` and `aud` claimsPrevents token reuse across services
Use HS256 minimum, RS256 for multi-serviceStronger signing for distributed systems
Never store secrets in JWT payloadJWTs are base64-encoded, not encrypted
Implement token revocation for admin actionsCritical for account compromise response

Multi-Factor Authentication

  • Enforce MFA for all admin and internal accounts from day one.
  • Offer MFA to all users, require it for sensitive operations (payment changes, data export).
  • Support TOTP (Google Authenticator, Authy) as baseline. Add WebAuthn/passkeys as the product matures.
  • Never use SMS-based MFA as the only option -- it is vulnerable to SIM swapping.

Authorization: Role-Based Access Control (RBAC)

Authentication tells you who someone is. Authorization tells you what they can do.

RBAC Implementation

<h1>Simple RBAC middleware for FastAPI</h1>

from enum import Enum

from functools import wraps

class Role(str, Enum):

ADMIN = "admin"

MEMBER = "member"

VIEWER = "viewer"

PERMISSIONS = {

Role.ADMIN: ["read", "write", "delete", "manage_users", "manage_billing"],

Role.MEMBER: ["read", "write"],

Role.VIEWER: ["read"],

}

def require_permission(permission: str):

def decorator(func):

@wraps(func)

async def wrapper(*args, **kwargs):

user = get_current_user() # From JWT/session

user_permissions = PERMISSIONS.get(user.role, [])

if permission not in user_permissions:

raise HTTPException(status_code=403, detail="Insufficient permissions")

return await func(*args, **kwargs)

return wrapper

return decorator

@app.delete("/api/services/{service_id}")

@require_permission("delete")

async def delete_service(service_id: int):

# Only admins and members with delete permission reach here

pass

RBAC checklist:

  • Default to least privilege: new users get Viewer role.
  • Log all privilege escalation events.
  • Review role assignments quarterly.
  • Never hardcode role checks by user ID -- always check the role.
  • For multi-tenant SaaS, always filter by `tenant_id` in every database query.

Review multi-tenant patterns in Services Overview.

Input Validation

Every piece of data that crosses a trust boundary (user input, API calls, webhooks) must be validated server-side.

<h1>Pydantic models for request validation (FastAPI)</h1>

from pydantic import BaseModel, Field, validator

import re

class CreateServiceRequest(BaseModel):

name: str = Field(..., min_length=3, max_length=63, pattern=r"^[a-z0-9][a-z0-9-]*[a-z0-9]$")

type: str = Field(..., pattern=r"^(compute

database
cache)$")

region: str = Field(..., pattern=r"^[a-z]{2}-[a-z]+-[0-9]$")

plan: str = Field(..., pattern=r"^(basic

standard
premium)$")

@validator("name")

def validate_name(cls, v):

if "--" in v:

raise ValueError("Name cannot contain consecutive hyphens")

reserved = ["admin", "system", "internal", "api"]

if v in reserved:

raise ValueError(f"Name '{v}' is reserved")

return v

Validation rules:

  • Validate type, length, format, and range for every field.
  • Use allowlists (what is permitted) not denylists (what is blocked).
  • Validate on the server even if you validate on the client.
  • Return generic error messages to users (do not leak internal details).

SQL Injection Prevention

SQL injection remains one of the most common and devastating attacks. Prevention is straightforward:

<h1>CORRECT: Parameterized queries (asyncpg)</h1>

user = await conn.fetchrow(

"SELECT * FROM users WHERE email = $1 AND tenant_id = $2",

email, tenant_id

)

<h1>CORRECT: ORM queries (SQLAlchemy)</h1>

user = session.query(User).filter(

User.email == email,

User.tenant_id == tenant_id

).first()

<h1>WRONG: String formatting (vulnerable to injection)</h1>

<h1>user = await conn.fetchrow(f"SELECT * FROM users WHERE email = '{email}'")</h1>

<h1>NEVER do this</h1>

ApproachSQL injection riskPerformance

Parameterized queriesNoneBest (prepared statements cached)
ORM with bound parametersNoneGood
Stored procedures with paramsNoneGood
String concatenationCritical vulnerabilityN/A -- never use
String formatting (f-strings)Critical vulnerabilityN/A -- never use

XSS (Cross-Site Scripting) Protection

XSS allows attackers to inject malicious scripts into your application that run in other users' browsers.

Prevention Layers

// React auto-escapes JSX content -- this is safe:

const UserName = ({ name }) => <span>{name}</span>;

// DANGEROUS: Never use dangerouslySetInnerHTML with user content

// <div dangerouslySetInnerHTML={{ __html: userInput }} /> // NEVER

// If you must render HTML, sanitize first:

import DOMPurify from "dompurify";

const cleanHTML = DOMPurify.sanitize(userInput, {

ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],

ALLOWED_ATTR: ["href"],

});

Server-side headers that prevent XSS:

<h1>Security headers middleware (FastAPI)</h1>

@app.middleware("http")

async def add_security_headers(request, call_next):

response = await call_next(request)

response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'"

response.headers["X-Content-Type-Options"] = "nosniff"

response.headers["X-Frame-Options"] = "DENY"

response.headers["X-XSS-Protection"] = "1; mode=block"

response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"

return response

Secrets Management

Secrets in source code are the number one cause of startup security breaches. The fix is simple but requires discipline:

<h1>.gitignore -- MUST include these</h1>

.env

.env.*

*.pem

*.key

kubeconfig*

<h1>Pre-commit hook to catch secrets</h1>

<h1>Install: pip install detect-secrets</h1>

<h1>Setup: detect-secrets scan > .secrets.baseline</h1>

<h1>Hook: detect-secrets-hook --baseline .secrets.baseline</h1>

Secrets management rules:

  • Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler).
  • Never commit secrets to git. Use `detect-secrets` or `gitleaks` as a pre-commit hook.
  • Separate secrets by environment (dev, staging, production).
  • Rotate credentials on a schedule: API keys every 90 days, database passwords every 180 days.
  • Immediately rotate any credential that may have been exposed.
  • Audit who has access to production secrets quarterly.

Reference secure deployment patterns in DevOps Tools.

Dependency Scanning

Your application is only as secure as its weakest dependency. Automate vulnerability scanning in CI:

<h1>GitHub Actions dependency scanning</h1>

name: Security Scan

on: [push, pull_request]

jobs:

scan:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

# Node.js dependency audit

- name: npm audit

run: npm audit --audit-level=high

# Python dependency scan

- name: pip-audit

run: |

pip install pip-audit

pip-audit -r requirements.txt

# Container image scan

- name: Trivy scan

uses: aquasecurity/trivy-action@master

with:

scan-type: fs

severity: CRITICAL,HIGH

exit-code: 1

# Secret detection

- name: Gitleaks

uses: gitleaks/gitleaks-action@v2

Scanning SLA by severity:

SeverityPatch SLAAction

Critical (CVSS 9.0+)24 hoursBlock deploys until fixed
High (CVSS 7.0-8.9)7 daysCreate ticket, prioritize
Medium (CVSS 4.0-6.9)30 daysSchedule in sprint
Low (CVSS 0.1-3.9)90 daysBatch with other updates

Incident Response Plan

Every startup needs an incident response plan before the first incident. Here is a minimal but effective framework:

Severity Definitions

SeverityDefinitionResponse timeExamples

SEV1Active data breach or complete outage15 minutesExposed database, ransomware, auth bypass
SEV2Security vulnerability with no confirmed exploit4 hoursCritical CVE in production dependency
SEV3Security improvement neededNext sprintMissing security header, audit finding

Incident Response Steps

  • Detect: Alert fires or report received.
  • Triage: Confirm the incident is real and assign severity.
  • Contain: Stop the bleeding (revoke credentials, block IP, disable feature).
  • Eradicate: Fix the root cause (patch vulnerability, remove malware).
  • Recover: Restore normal operations and verify.
  • Review: Blameless post-mortem within 48 hours. Document what happened, what went well, what to improve.
  • Communication During Security Incidents

    • Notify the relevant supervisory authority within 72 hours of becoming aware of a breach (GDPR Art. 33, if applicable); notify affected individuals without undue delay when the breach poses high risk to their rights (GDPR Art. 34).
    • Use pre-written templates to avoid delays.
    • Designate one person as the external communicator.
    • Log all actions taken with timestamps for potential legal proceedings.

    Review incident response documentation at Documentation.

    Security Implementation Priority Order

    For a startup with limited engineering time, implement controls in this order:

    PriorityControlEffortImpact

    Week 1HTTPS everywhere + security headers2 hoursHigh
    Week 1Parameterized queries (no SQL injection)2 hoursCritical
    Week 1Secrets out of source code4 hoursCritical
    Week 2MFA for admin accounts4 hoursHigh
    Week 2Input validation on all endpoints8 hoursHigh
    Week 2Dependency scanning in CI2 hoursMedium
    Week 3RBAC implementation8 hoursHigh
    Week 3CSP and XSS headers2 hoursMedium
    Week 4Incident response plan documented4 hoursHigh
    Week 4Secret rotation schedule2 hoursMedium

    Frequently Asked Questions

    Do startups really need to worry about security this early?

    Yes. Startups are increasingly targeted because attackers know early-stage companies have weak security. A data breach in the first two years can kill a startup outright through customer trust loss, legal liability, and remediation costs. The controls in this checklist take 2-4 weeks of part-time effort and prevent 90% of common attacks.

    Should we get SOC 2 certified?

    Not immediately. Focus on the security controls first. When enterprise customers start requiring SOC 2 in procurement (usually Series A and beyond), you will already have the technical controls in place. The certification process then becomes a documentation exercise rather than a technical scramble.

    Is it safe to use JWTs for authentication?

    JWTs are safe when implemented correctly: short expiration (1 hour or less), explicit algorithm verification, proper secret management, and a token revocation mechanism for admin actions. The most common JWT vulnerability is not the format itself but poor implementation (no expiration, algorithm confusion, secrets in payload).

    How do we handle security for third-party integrations?

    Apply the same input validation to webhook payloads that you apply to user input. Verify webhook signatures. Use separate API keys per integration with minimal required permissions. Monitor third-party API usage for anomalies. Review third-party access quarterly and revoke unused integrations.

    What is the biggest security mistake startups make?

    Storing secrets in source code. This includes database passwords in config files committed to git, API keys in frontend JavaScript, and deployment credentials in CI/CD configs without proper secret management. One exposed repository makes all those secrets public permanently (git history is forever).

    How much should a startup spend on security tooling?

    Zero to $50/month in the first year. Most critical security tools are free: OWASP ZAP for scanning, detect-secrets for secret detection, npm audit and pip-audit for dependency scanning, Let's Encrypt for TLS. Paid tools become worthwhile when you need continuous monitoring or compliance automation.

    Final Takeaway

    Strong startup security is built from consistent fundamentals: authentication with MFA, authorization with RBAC, parameterized queries, input validation, secrets management, dependency scanning, and a practiced incident response plan. You do not need a security team to implement these controls. You need a checklist, 2-4 weeks of focused effort, and the discipline to maintain them as the product grows.

    For architecture alignment and security review, visit Services Overview or contact us.