Skip to content

Security Standards

OWASP Top 10 Coverage

Checklist mapping how this architecture addresses OWASP Top 10 (2021):

#VulnerabilityMitigationStatus
A01Broken Access ControlRBAC with guards, row-level checks, JWT validation✅ Covered
A02Cryptographic FailuresAES-256-GCM encryption, bcrypt passwords, TLS required✅ Covered
A03InjectionPrisma parameterized queries, input validation, no raw SQL✅ Covered
A04Insecure DesignClean Architecture, threat modeling in design phase✅ Covered
A05Security MisconfigurationHelmet headers, CORS whitelist, env validation✅ Covered
A06Vulnerable Componentsnpm audit in CI, Dependabot alerts⚠️ Requires CI setup
A07Auth FailuresJWT + refresh tokens, rate limiting, account lockout✅ Covered
A08Data Integrity FailuresWebhook signature verification, idempotency keys✅ Covered
A09Logging FailuresStructured logging, audit trails, sensitive data redaction✅ Covered
A10SSRFNo 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 TypeStorageEncryption Method
PasswordsDatabasebcrypt (one-way hash)
Session tokensDatabaseSHA256 hash
KYC documentsObject storage (S3)AES-256-GCM + encrypted bucket
Payout detailsDatabase (JSONB)Application-level AES-256-GCM
Tax IDsDatabaseApplication-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

AspectImplementation
Master key storageEnvironment variable (AWS Secrets Manager in production)
Key rotationSupport for versioned keys with gradual re-encryption
Key accessOnly application service layer, never exposed to API
Backup encryptionSeparate 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

DataMethodReason
PasswordHash (bcrypt)Never need original
Tax IDEncrypt + HashNeed original for display, hash for lookup
Payout accountEncryptNeed original for payouts
KYC documentEncrypt (at rest)Need original for review
Session tokenHashOnly need to verify
Device fingerprintHashOnly 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
EnvironmentRecommendation
DevelopmentDisabled (empty list)
StagingOffice IPs + VPN range
ProductionOffice IPs + VPN range only

Security Audit Plan

Pre-Launch Checklist

CheckTool/MethodFrequency
Dependency vulnerabilitiesnpm audit, SnykEvery build
Secret scanningGitHub secret scanning, gitleaksEvery commit
SAST (Static Analysis)SonarQube, CodeQLWeekly
Container scanningTrivy, Docker ScoutEvery build
Infrastructure reviewManual checklistBefore launch

Post-Launch Schedule

ActivityFrequencyScope
Automated vulnerability scanWeeklyDependencies, containers
Manual security reviewQuarterlyCode changes since last review
Penetration testAnnuallyFull application + infrastructure
Access auditMonthlyAdmin accounts, API keys, DB access
Incident response drillSemi-annuallyTeam 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

MetricTargetAlert Threshold
Time to patch critical CVE< 24 hours> 48 hours
Failed login attemptsBaseline> 5x baseline
Admin access from new IP0 unexpectedAny unexpected
Webhook signature failures< 0.1%> 1%
Rate limit triggersBaseline> 10x baseline