SaaS Security Checklist: OWASP Top 10, Security Headers, Dependency Scanning, and Pen Testing
Comprehensive SaaS security checklist for 2026 — OWASP Top 10 mitigations, HTTP security headers, dependency vulnerability scanning, secrets management, CSP con
SaaS Security Checklist: OWASP Top 10, Security Headers, Dependency Scanning, and Pen Testing
Security for SaaS products isn't a single feature — it's a set of controls applied consistently across the stack. Most SaaS security breaches aren't the result of sophisticated attacks; they're unpatched dependencies, missing authorization checks, and secrets committed to Git.
This checklist covers the controls with the highest return per engineering hour.
OWASP Top 10: 2025 Edition
| Risk | Description | Primary Mitigation |
|---|---|---|
| A01: Broken Access Control | Users accessing data/actions they shouldn't | Authorization checks on every route, row-level security |
| A02: Cryptographic Failures | Sensitive data exposed due to weak/missing encryption | TLS everywhere, encrypted secrets, bcrypt for passwords |
| A03: Injection | SQL, command, and other injection attacks | Parameterized queries, ORMs, input validation |
| A04: Insecure Design | Missing threat modeling, unsafe business logic | Design-time review, abuse case testing |
| A05: Security Misconfiguration | Default configs, verbose error messages, open ports | Hardened defaults, IaC security checks |
| A06: Vulnerable Components | Outdated libraries with known CVEs | Automated dependency scanning, update policies |
| A07: Authentication Failures | Weak passwords, broken session management | MFA, secure session handling, account lockout |
| A08: Software Integrity Failures | CI/CD pipeline compromise, unsigned packages | Pinned dependencies, signed releases, SLSA |
| A09: Security Logging Failures | No audit trail for security events | Log authentication events, access to sensitive data |
| A10: SSRF | Server-side requests to internal resources | URL allowlisting, block private IP ranges |
A01: Authorization — The Most Common Failure
Authorization is the most frequently broken control. Every API endpoint needs:
- Authentication check (is there a valid session?)
- Authorization check (is this user allowed to do this thing?)
// middleware/authorize.ts
import type { FastifyRequest, FastifyReply } from 'fastify';
interface AuthorizeOptions {
resource: string;
action: 'read' | 'write' | 'delete' | 'admin';
}
export function authorize(opts: AuthorizeOptions) {
return async (request: FastifyRequest, reply: FastifyReply) => {
const user = request.user; // Set by JWT auth middleware
if (!user) {
return reply.code(401).send({ error: 'Authentication required' });
}
const allowed = await checkPermission({
userId: user.id,
tenantId: user.tenantId,
resource: opts.resource,
action: opts.action,
});
if (!allowed) {
// Log authorization failure — this is a security event
request.log.warn({
event: 'authorization_denied',
userId: user.id,
tenantId: user.tenantId,
resource: opts.resource,
action: opts.action,
path: request.url,
});
return reply.code(403).send({ error: 'Forbidden' });
}
};
}
// Route with authorization
app.delete('/api/documents/:id',
{ preHandler: [authenticate, authorize({ resource: 'documents', action: 'delete' })] },
async (request, reply) => {
const doc = await db.documents.findById(request.params.id);
// CRITICAL: verify the document belongs to the current tenant
// Authorization middleware checked role, but not ownership
if (doc.tenantId !== request.user.tenantId) {
throw new ForbiddenError('Document not in your organization');
}
await db.documents.delete(request.params.id);
return reply.code(204).send();
}
);
The insidious form of A01: Role check passes, but object ownership check is missing. User A can delete User B's documents because the delete endpoint only checks "is user an admin?" not "does the document belong to this user's tenant?"
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
A03: Injection — Parameterized Queries Always
// ❌ NEVER: String interpolation in SQL
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// ✅ Parameterized query — always safe
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
// ✅ ORM handles parameterization automatically
const user = await prisma.user.findUnique({ where: { email } });
// ❌ NEVER: OS command injection
const output = exec(`convert ${filename} -resize 200x200 output.jpg`);
// ✅ Pass arguments as array (never shell-interpolated)
const output = execFile('convert', [filename, '-resize', '200x200', 'output.jpg']);
HTTP Security Headers
Security headers are the fastest security wins — configure once, protect all pages:
// middleware/security-headers.ts (Fastify)
app.addHook('onSend', async (request, reply) => {
// Prevent clickjacking
reply.header('X-Frame-Options', 'DENY');
// Prevent MIME type sniffing
reply.header('X-Content-Type-Options', 'nosniff');
// Force HTTPS for 1 year, including subdomains
reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Referrer policy — don't leak auth tokens in Referer header
reply.header('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions policy — disable unused browser APIs
reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
// Content Security Policy
reply.header('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self' 'nonce-{NONCE}'", // Replace {NONCE} with per-request nonce
"style-src 'self' 'unsafe-inline'", // unsafe-inline needed for Tailwind in prod
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.yourproduct.com wss://api.yourproduct.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join('; '));
});
Test your headers: securityheaders.com will grade your current headers. Aim for A+.
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Secrets Management
Secrets committed to Git are the most common cause of cloud account compromise:
# Install git-secrets to prevent commits
brew install git-secrets
git secrets --install # Install hooks in current repo
git secrets --register-aws # Add AWS key patterns
# Or use detect-secrets
pip install detect-secrets
detect-secrets scan > .secrets.baseline
# Add to pre-commit hook:
detect-secrets-hook --baseline .secrets.baseline
// ✅ Load secrets from environment (set by AWS Secrets Manager, 1Password, etc.)
const stripeKey = process.env.STRIPE_SECRET_KEY;
if (!stripeKey) throw new Error('STRIPE_SECRET_KEY is not set');
// For AWS: use Secrets Manager SDK in production
// secrets are injected into ECS task environment at runtime
// Never store them in task definition JSON or Terraform state
# terraform/ecs-task.tf
# ✅ Reference secrets from Secrets Manager — value never appears in Terraform
resource "aws_ecs_task_definition" "api" {
# ...
container_definitions = jsonencode([{
name = "api"
secrets = [
{
name = "STRIPE_SECRET_KEY"
valueFrom = "arn:aws:secretsmanager:us-east-1:xxx:secret:prod/stripe-key"
},
{
name = "DATABASE_URL"
valueFrom = "arn:aws:secretsmanager:us-east-1:xxx:secret:prod/database-url"
}
]
}])
}
Dependency Scanning
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 6 * * 1' # Weekly Monday morning scan
jobs:
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: pnpm audit
run: pnpm audit --audit-level=high
# Fails CI if high or critical vulnerabilities exist
- name: Snyk vulnerability scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep SAST
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/typescript
p/owasp-top-ten
p/sql-injection
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Gitleaks secret scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Rate Limiting and Brute Force Prevention
// lib/rate-limit.ts
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
export async function checkRateLimit(
identifier: string, // IP or user ID
action: string, // 'login', 'api', 'password-reset'
limit: number,
windowSeconds: number,
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
const key = `rate:${action}:${identifier}`;
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart);
pipeline.zadd(key, now, `${now}`);
pipeline.zcard(key);
pipeline.expire(key, windowSeconds);
const results = await pipeline.exec();
const count = (results?.[2]?.[1] as number) ?? 0;
const allowed = count <= limit;
const resetAt = now + windowSeconds * 1000;
if (!allowed) {
// Log brute force attempt
console.warn({ event: 'rate_limit_exceeded', identifier, action, count });
}
return { allowed, remaining: Math.max(0, limit - count), resetAt };
}
// Protect login endpoint
app.post('/api/auth/login', async (request, reply) => {
const ip = request.ip;
const { email } = request.body as { email: string };
// 5 attempts per 15 minutes per IP
const ipLimit = await checkRateLimit(ip, 'login', 5, 900);
if (!ipLimit.allowed) {
reply.header('Retry-After', String(Math.ceil((ipLimit.resetAt - Date.now()) / 1000)));
return reply.code(429).send({ error: 'Too many login attempts. Try again later.' });
}
// 10 attempts per hour per email (catch distributed brute force)
const emailLimit = await checkRateLimit(email, 'login-email', 10, 3600);
if (!emailLimit.allowed) {
return reply.code(429).send({ error: 'Too many login attempts for this account.' });
}
// ...proceed with auth
});
Security Review Checklist
Before any feature ships:
Pre-Ship Security Checklist
Authentication & Authorization
- All API routes require authentication (explicit opt-out for public routes)
- Authorization checks verify resource ownership, not just role
- Admin endpoints require explicit admin role check
- JWT tokens have appropriate expiry (15-60 min for access tokens)
Data Handling
- No sensitive data (passwords, keys, PII) logged
- Database queries use parameterized statements or ORM
- File uploads validated: type, size, content (magic bytes check)
- User-provided HTML sanitized with DOMPurify before rendering
Secrets
- No secrets in source code, comments, or config files
-
git secretsscan passes - New secrets added to Secrets Manager, not .env files
HTTP
- Security headers present (test with securityheaders.com)
- CORS configured for specific origins (not
*) - Rate limiting on auth and sensitive endpoints
- HTTPS enforced (no HTTP in production)
Dependencies
-
pnpm auditpasses with no high/critical - No new GPL-licensed libraries added without legal review
---
## Working With Viprasol
We conduct security reviews for SaaS products — OWASP Top 10 audits, security header configuration, dependency scanning setup, secrets management migration, authorization model review, and penetration test preparation.
→ [Talk to our team](/contact) about application security review.
---
See Also
- Compliance and SOC 2 — security controls in a compliance context
- Zero Trust Security — network-level security model
- Data Privacy Engineering — GDPR compliance and data handling
- API Rate Limiting — rate limiting implementation patterns
- Web Development Services — secure SaaS product development
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.