Security Standards
OWASP Top 10 Coverage
Checklist mapping how this architecture addresses OWASP Top 10 (2021):
| # | Vulnerability | Mitigation | Status |
|---|---|---|---|
| A01 | Broken Access Control | RBAC with guards, row-level checks, JWT validation | ✅ Covered |
| A02 | Cryptographic Failures | AES-256-GCM encryption, bcrypt passwords, TLS required | ✅ Covered |
| A03 | Injection | Prisma parameterized queries, input validation, no raw SQL | ✅ Covered |
| A04 | Insecure Design | Clean Architecture, threat modeling in design phase | ✅ Covered |
| A05 | Security Misconfiguration | Helmet headers, CORS whitelist, env validation | ✅ Covered |
| A06 | Vulnerable Components | npm audit in CI, Dependabot alerts | ⚠️ Requires CI setup |
| A07 | Auth Failures | JWT + refresh tokens, rate limiting, account lockout | ✅ Covered |
| A08 | Data Integrity Failures | Webhook signature verification, idempotency keys | ✅ Covered |
| A09 | Logging Failures | Structured logging, audit trails, sensitive data redaction | ✅ Covered |
| A10 | SSRF | No user-controlled URLs in backend requests | ✅ N/A by design |
Dependency Scanning (A06)
yaml
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 0 * * 1' # Weekly
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}Password Policy
typescript
// Minimum password requirements
const PASSWORD_POLICY = {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecialChar: false, // Optional for better UX
};
// Validation with class-validator
@IsString()
@MinLength(8)
@MaxLength(128)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase, lowercase, and number',
})
password: string;Account Lockout
typescript
// After 5 failed login attempts in 15 minutes:
// - Lock account for 30 minutes
// - Send security notification email
const LOCKOUT_POLICY = {
maxAttempts: 5,
windowMinutes: 15,
lockoutMinutes: 30,
notifyOnLockout: true,
};CORS Configuration
typescript
// main.ts
app.enableCors({
origin: configService.get('FRONTEND_URL'),
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
credentials: true,
maxAge: 86400, // 24 hours
});Security Headers (Helmet)
typescript
// main.ts
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // For Swagger UI
imgSrc: ["'self'", 'data:', 'https:'],
scriptSrc: ["'self'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
}));Input Sanitization
typescript
// Use class-transformer and class-validator globally
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform types
transformOptions: {
enableImplicitConversion: true,
},
}));
// HTML sanitization for text fields
import { sanitize } from 'class-sanitizer';
@Transform(({ value }) => sanitize(value))
@IsString()
description: string;Sensitive Data in Logs
typescript
// NEVER log:
// - Passwords
// - API keys/tokens
// - Full credit card numbers
// - Full KYC documents
// - Session tokens
// Sanitize before logging
const sanitizeForLogging = (obj: Record<string, unknown>): Record<string, unknown> => {
const sensitiveFields = ['password', 'token', 'apiKey', 'secret', 'cardNumber'];
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
if (sensitiveFields.some(f => key.toLowerCase().includes(f.toLowerCase()))) {
return [key, '[REDACTED]'];
}
return [key, value];
})
);
};Data Encryption Strategy
Overview
The platform handles sensitive data requiring encryption:
- KYC Documents: Passport scans, ID photos
- Payment Details: Payout account numbers
- Personal Data: Tax IDs, phone numbers (PII)
Encryption Approach
| Data Type | Storage | Encryption Method |
|---|---|---|
| Passwords | Database | bcrypt (one-way hash) |
| Session tokens | Database | SHA256 hash |
| KYC documents | Object storage (S3) | AES-256-GCM + encrypted bucket |
| Payout details | Database (JSONB) | Application-level AES-256-GCM |
| Tax IDs | Database | Application-level AES-256-GCM |
Field-Level Encryption (pgcrypto)
For sensitive fields stored directly in PostgreSQL:
sql
-- Enable pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Example: Encrypted tax ID storage
CREATE TABLE core.tax_identifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES core.users(id),
tax_id_type VARCHAR(20) NOT NULL, -- SSN, TIN, ITIN
tax_id_encrypted BYTEA NOT NULL, -- Encrypted with pgcrypto
tax_id_hash VARCHAR(64) NOT NULL, -- For lookups (SHA256)
jurisdiction VARCHAR(3) NOT NULL, -- Country code
verified_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Unique index on hash for duplicate prevention
CREATE UNIQUE INDEX idx_tax_id_hash ON core.tax_identifications(tax_id_hash);Application-Level Encryption Service
typescript
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
import { promisify } from 'util';
const scryptAsync = promisify(scrypt);
export class EncryptionService {
private readonly algorithm = 'aes-256-gcm';
private readonly keyLength = 32;
private readonly ivLength = 16;
private readonly tagLength = 16;
constructor(private readonly masterKey: string) {}
async encrypt(plaintext: string): Promise<string> {
// Derive key from master key using scrypt
const salt = randomBytes(16);
const key = (await scryptAsync(this.masterKey, salt, this.keyLength)) as Buffer;
// Generate IV
const iv = randomBytes(this.ivLength);
// Encrypt
const cipher = createCipheriv(this.algorithm, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
// Combine: salt(16) + iv(16) + tag(16) + ciphertext
const combined = Buffer.concat([salt, iv, tag, encrypted]);
return combined.toString('base64');
}
async decrypt(ciphertext: string): Promise<string> {
const combined = Buffer.from(ciphertext, 'base64');
// Extract components
const salt = combined.subarray(0, 16);
const iv = combined.subarray(16, 32);
const tag = combined.subarray(32, 48);
const encrypted = combined.subarray(48);
// Derive key
const key = (await scryptAsync(this.masterKey, salt, this.keyLength)) as Buffer;
// Decrypt
const decipher = createDecipheriv(this.algorithm, key, iv);
decipher.setAuthTag(tag);
return decipher.update(encrypted) + decipher.final('utf8');
}
hash(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
}Key Management
| Aspect | Implementation |
|---|---|
| Master key storage | Environment variable (AWS Secrets Manager in production) |
| Key rotation | Support for versioned keys with gradual re-encryption |
| Key access | Only application service layer, never exposed to API |
| Backup encryption | Separate key, stored in different location |
Encryption Configuration
typescript
// config/encryption.schema.ts
export const encryptionConfigSchema = z.object({
ENCRYPTION_MASTER_KEY: z.string().min(32),
ENCRYPTION_KEY_VERSION: z.string().default('v1'),
KYC_BUCKET_ENCRYPTION_KEY_ARN: z.string().optional(),
});What to Encrypt vs Hash
| Data | Method | Reason |
|---|---|---|
| Password | Hash (bcrypt) | Never need original |
| Tax ID | Encrypt + Hash | Need original for display, hash for lookup |
| Payout account | Encrypt | Need original for payouts |
| KYC document | Encrypt (at rest) | Need original for review |
| Session token | Hash | Only need to verify |
| Device fingerprint | Hash | Only need to verify |
Admin Endpoint Protection
IP Allowlisting for Admin Panel
Admin endpoints (/api/v1/admin/*) should be restricted to known IP addresses in production.
typescript
// middleware/admin-ip-allowlist.middleware.ts
import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AdminIpAllowlistMiddleware implements NestMiddleware {
private readonly allowedIps: string[];
constructor(private readonly config: ConfigService) {
// Load from environment: ADMIN_ALLOWED_IPS=1.2.3.4,5.6.7.8,10.0.0.0/8
const ipsConfig = this.config.get<string>('ADMIN_ALLOWED_IPS', '');
this.allowedIps = ipsConfig.split(',').filter(Boolean);
}
use(req: Request, res: Response, next: NextFunction) {
// Skip in development
if (this.config.get('NODE_ENV') === 'development') {
return next();
}
// Skip if no IPs configured (allows gradual rollout)
if (this.allowedIps.length === 0) {
return next();
}
const clientIp = this.getClientIp(req);
if (!this.isIpAllowed(clientIp)) {
throw new ForbiddenException('Access denied: IP not in allowlist');
}
next();
}
private getClientIp(req: Request): string {
// Handle proxies (X-Forwarded-For)
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
return (forwarded as string).split(',')[0].trim();
}
return req.ip || req.socket.remoteAddress || '';
}
private isIpAllowed(ip: string): boolean {
return this.allowedIps.some(allowed => {
if (allowed.includes('/')) {
// CIDR notation (e.g., 10.0.0.0/8)
return this.isIpInCidr(ip, allowed);
}
return ip === allowed;
});
}
private isIpInCidr(ip: string, cidr: string): boolean {
// Implementation for CIDR matching
const [range, bits] = cidr.split('/');
const mask = ~(2 ** (32 - parseInt(bits)) - 1);
const ipNum = this.ipToNumber(ip);
const rangeNum = this.ipToNumber(range);
return (ipNum & mask) === (rangeNum & mask);
}
private ipToNumber(ip: string): number {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
}
}Applying to Admin Routes
typescript
// app.module.ts
@Module({
imports: [...],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AdminIpAllowlistMiddleware)
.forRoutes({ path: 'api/v1/admin/*', method: RequestMethod.ALL });
}
}Configuration
bash
# .env.production
ADMIN_ALLOWED_IPS=203.0.113.50,198.51.100.0/24,10.0.0.0/8| Environment | Recommendation |
|---|---|
| Development | Disabled (empty list) |
| Staging | Office IPs + VPN range |
| Production | Office IPs + VPN range only |
Security Audit Plan
Pre-Launch Checklist
| Check | Tool/Method | Frequency |
|---|---|---|
| Dependency vulnerabilities | npm audit, Snyk | Every build |
| Secret scanning | GitHub secret scanning, gitleaks | Every commit |
| SAST (Static Analysis) | SonarQube, CodeQL | Weekly |
| Container scanning | Trivy, Docker Scout | Every build |
| Infrastructure review | Manual checklist | Before launch |
Post-Launch Schedule
| Activity | Frequency | Scope |
|---|---|---|
| Automated vulnerability scan | Weekly | Dependencies, containers |
| Manual security review | Quarterly | Code changes since last review |
| Penetration test | Annually | Full application + infrastructure |
| Access audit | Monthly | Admin accounts, API keys, DB access |
| Incident response drill | Semi-annually | Team readiness |
Penetration Testing Scope
When engaging penetration testers, include:
In Scope:
- Authentication and session management
- Authorization and access control (RBAC bypass)
- Payment flow manipulation
- Commission calculation tampering
- MLM tree manipulation attempts
- API rate limiting bypass
- Webhook forgery attempts
- Admin panel access
Out of Scope:
- Physical security
- Social engineering (unless agreed)
- DDoS attacks
- Third-party services (Stripe, payment providers)
Vulnerability Disclosure
markdown
# SECURITY.md (in repository root)
## Reporting Security Issues
Please report security vulnerabilities to: security@iwm-platform.com
DO NOT create public GitHub issues for security vulnerabilities.
We will acknowledge receipt within 48 hours and provide
a detailed response within 7 days.Security Metrics to Track
| Metric | Target | Alert Threshold |
|---|---|---|
| Time to patch critical CVE | < 24 hours | > 48 hours |
| Failed login attempts | Baseline | > 5x baseline |
| Admin access from new IP | 0 unexpected | Any unexpected |
| Webhook signature failures | < 0.1% | > 1% |
| Rate limit triggers | Baseline | > 10x baseline |