Security-by-Default for Fintech MVPs: Threat Modeling in a Week
Fintech MVPs can't skip security. Learn to conduct a one-week threat modeling sprint that identifies critical risks and implements baseline controls.
Spend one week on threat modeling before launching your fintech MVP. Day 1: Map data flows. Day 2: Identify threats using STRIDE. Day 3: Prioritize by risk. Day 4: Implement baseline controls. Day 5: Set up logging and monitoring. This isn't security theater—it's the minimum viable security that protects your customers and your company.
Day 1: Map Your Data Flows
Before identifying threats, you need to understand what you're protecting. Spend day one mapping how data moves through your system.What to document:Create a simple diagram showing:- Where user data enters the system (APIs, forms, webhooks)- How data moves between components (frontend → backend → database)- Where sensitive data is stored- What external services you integrate with- Where data exits the system (exports, APIs, notifications)Example data flow diagram for a payment app:
1 ┌──────────────────────────────────────────────────────────────────┐ 2 │ DATA FLOW MAP │ 3 ├──────────────────────────────────────────────────────────────────┤ 4 │ │ 5 │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 6 │ │ Mobile │────▶│ API │────▶│ Backend │────▶│ Database│ │ 7 │ │ App │ │ Gateway │ │ Service │ │(Postgres)│ │ 8 │ └─────────┘ └─────────┘ └────┬────┘ └─────────┘ │ 9 │ │ │ │ 10 │ │ User Data: │ │ 11 │ │ • Email, name │ Financial Data: │ 12 │ │ • Password │ • Account balances │ 13 │ │ • Phone number │ • Transaction history │ 14 │ │ │ • Bank connections │ 15 │ │ │ │ 16 │ │ ┌────────────────┼───────────────┐ │ 17 │ │ │ │ │ │ 18 │ │ ▼ ▼ ▼ │ 19 │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 20 │ │ │ Plaid │ │ Stripe │ │ Email │ │ 21 │ │ │ (Bank) │ │(Payment)│ │ Service │ │ 22 │ │ └─────────┘ └─────────┘ └─────────┘ │ 23 │ │ │ 24 └───────┼──────────────────────────────────────────────────────────┘ 25 │ 26 │ DATA CLASSIFICATION: 27 │ 🔴 Critical: Passwords, bank credentials, payment tokens 28 │ 🟠 Sensitive: PII(name, email, phone), transaction data 29 │ 🟢 Standard: App preferences, non-PII metadata
Questions to answer:1. Where does PII (personally identifiable information) live?2. Where do financial credentials flow?3. What happens if each external service is compromised?4. Where could an attacker intercept data?Deliverable: A visual diagram and a table listing every data store and what it contains.
Day 2: Identify Threats (STRIDE)
Use the STRIDE framework to systematically identify threats. For each component in your data flow, ask these six questions:S - Spoofing (Identity)Can an attacker pretend to be someone else?- Weak password requirements allowing brute force- Missing multi-factor authentication- API endpoints without authentication- Forged email sender addressesT - Tampering (Data Integrity)Can an attacker modify data they shouldn't?- SQL injection modifying database records- Man-in-the-middle attacks on HTTP connections- Unsigned webhooks allowing forged events- Client-side validation only (no server validation)R - Repudiation (Non-repudiation)Can users deny actions they took?- No audit logs of transactions- Logs that can be modified- Missing timestamps on records- No evidence trail for disputesI - Information DisclosureCan an attacker access data they shouldn't?- Database credentials in code- Error messages revealing system details- Logs containing sensitive data- Unencrypted data at restD - Denial of ServiceCan an attacker make the system unavailable?- No rate limiting on APIs- Resource-intensive operations without limits- Single points of failure- No protection against DDoSE - Elevation of PrivilegeCan an attacker gain permissions they shouldn't have?- Missing authorization checks on endpoints- Insecure direct object references (IDOR)- JWT tokens without expiration- Admin functions accessible to regular usersSTRIDE Analysis Example:| Component | Threat Type | Specific Threat | Risk Level ||-----------|-------------|-----------------|------------|| Login API | Spoofing | Brute force attack | High || Login API | Information Disclosure | Timing attacks reveal valid emails | Medium || Payment API | Tampering | Unsigned webhooks | Critical || Transaction Log | Repudiation | Logs can be deleted | High || User Database | Information Disclosure | SQL injection | Critical || All APIs | DoS | No rate limiting | Medium || Admin Panel | Elevation of Privilege | IDOR allows access to other users | Critical |Deliverable: A table mapping each component to potential threats.
Day 3: Risk Prioritization
You can't fix everything at once. Prioritize based on impact and likelihood.Risk Matrix:
1 LIKELIHOOD 2 Low Medium High 3 High │ Med │ High │ Critical │ 4 IMPACT Medium │ Low │ Med │ High │ 5 Low │ Low │ Low │ Med │
Score each threat:| Threat | Impact | Likelihood | Risk Level | Priority ||--------|--------|------------|------------|----------|| SQL injection on payments | Critical | Medium | High | P1 || Brute force login | High | High | Critical | P1 || Unsigned webhooks | Critical | Medium | High | P1 || Missing rate limits | Medium | High | High | P2 || No audit logs | High | Low | Medium | P2 || Error message leakage | Low | High | Medium | P3 |P1 (Fix before launch):- Any threat that could lead to financial loss- Authentication bypasses- Data breaches affecting PII or financial dataP2 (Fix within first month):- Threats that could enable P1 threats- Availability concerns- Compliance requirementsP3 (Track and schedule):- Lower impact threats- Defense-in-depth measures- Nice-to-have security featuresDeliverable: Prioritized list of threats to address.
Day 4: Baseline Security Controls
Implement the minimum security controls every fintech MVP needs.1. Encryption in Transit (TLS)
1 // Enforce HTTPS in production
2 if (process.env.NODE_ENV === 'production') {
3 app.use((req, res, next) => {
4 if (req.headers['x-forwarded-proto'] !== 'https') {
5 return res.redirect(`https://${req.headers.host}${req.url}`);
6 }
7 next();
8 });
9 }
10
11 // Security headers
12 app.use(helmet({
13 contentSecurityPolicy: true,
14 hsts: { maxAge: 31536000, includeSubDomains: true },
15 }));1 // Use database-level encryption(PostgreSQL example)
2 // Enable in your PostgreSQL configuration or use managed service
3 // AWS RDS: Enable encryption when creating instance
4 // Or encrypt specific columns:
5
6 import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
7
8 const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // 32 bytes
9 const ALGORITHM = 'aes-256-gcm';
10
11 function encrypt(text: string): string {
12 const iv = randomBytes(16);
13 const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
14 let encrypted = cipher.update(text, 'utf8', 'hex');
15 encrypted += cipher.final('hex');
16 const authTag = cipher.getAuthTag();
17 return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
18 }
19
20 function decrypt(encryptedText: string): string {
21 const [ivHex, authTagHex, encrypted] = encryptedText.split(':');
22 const decipher = createDecipheriv(
23 ALGORITHM,
24 Buffer.from(ENCRYPTION_KEY, 'hex'),
25 Buffer.from(ivHex, 'hex')
26 );
27 decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
28 let decrypted = decipher.update(encrypted, 'hex', 'utf8');
29 decrypted += decipher.final('utf8');
30 return decrypted;
31 }1 // Password requirements
2 const PASSWORD_REQUIREMENTS = {
3 minLength: 12,
4 requireUppercase: true,
5 requireLowercase: true,
6 requireNumbers: true,
7 requireSymbols: true,
8 };
9
10 // Rate limiting on login
11 const loginLimiter = rateLimit({
12 windowMs: 15 * 60 * 1000, // 15 minutes
13 max: 5, // 5 attempts
14 message: 'Too many login attempts, please try again later',
15 });
16 app.use('/api/auth/login', loginLimiter);
17
18 // Bcrypt for password hashing
19 import bcrypt from 'bcrypt';
20 const SALT_ROUNDS = 12;
21
22 async function hashPassword(password: string): Promise<string> {
23 return bcrypt.hash(password, SALT_ROUNDS);
24 }1 import { z } from 'zod';
2
3 // Define strict schemas for all inputs
4 const TransferSchema = z.object({
5 amount: z.number().positive().max(1000000),
6 recipientId: z.string().uuid(),
7 memo: z.string().max(500).optional(),
8 });
9
10 app.post('/api/transfer', async (req, res) => {
11 const result = TransferSchema.safeParse(req.body);
12 if (!result.success) {
13 return res.status(400).json({ error: 'Invalid input', details: result.error });
14 }
15 // Process with validated data
16 const { amount, recipientId, memo } = result.data;
17 });1 // ❌ NEVER do this
2 const query = `SELECT * FROM users WHERE id = '${userId}'`;
3
4 // ✅ Use parameterized queries
5 const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
6
7 // ✅ Or use an ORM(Prisma, Drizzle)
8 const user = await prisma.user.findUnique({ where: { id: userId } });1 // Check ownership on every resource access
2 async function getTransaction(userId: string, transactionId: string) {
3 const transaction = await db.transaction.findUnique({
4 where: { id: transactionId }
5 });
6
7 // Always verify ownership
8 if (transaction.userId !== userId) {
9 throw new ForbiddenError('Access denied');
10 }
11
12 return transaction;
13 }Day 5: Logging and Monitoring
You can't detect attacks without visibility. Set up security logging and monitoring on day 5.What to Log:| Event Type | What to Capture | Why ||------------|-----------------|-----|| Authentication | Login attempts (success/fail), IP, user agent | Detect brute force || Authorization | Access denied events | Detect privilege escalation attempts || Transactions | All financial operations | Fraud detection, compliance || Admin actions | All admin operations | Insider threat detection || API errors | 4xx and 5xx responses | Attack reconnaissance || Data access | Sensitive data queries | Data breach detection |Structured Logging Implementation:
1 import pino from 'pino';
2
3 const logger = pino({
4 level: 'info',
5 redact: ['password', 'token', 'cardNumber', 'cvv', 'ssn'], // Never log these
6 });
7
8 // Security event logging
9 function logSecurityEvent(event: SecurityEvent) {
10 logger.info({
11 type: 'security',
12 event: event.type,
13 userId: event.userId,
14 ip: event.ip,
15 userAgent: event.userAgent,
16 success: event.success,
17 metadata: event.metadata,
18 timestamp: new Date().toISOString(),
19 });
20 }
21
22 // Example: Log login attempt
23 app.post('/api/auth/login', async (req, res) => {
24 const { email, password } = req.body;
25
26 try {
27 const user = await authenticate(email, password);
28
29 logSecurityEvent({
30 type: 'LOGIN_SUCCESS',
31 userId: user.id,
32 ip: req.ip,
33 userAgent: req.headers['user-agent'],
34 success: true,
35 });
36
37 res.json({ token: generateToken(user) });
38 } catch (error) {
39 logSecurityEvent({
40 type: 'LOGIN_FAILURE',
41 userId: null,
42 ip: req.ip,
43 userAgent: req.headers['user-agent'],
44 success: false,
45 metadata: { email, reason: error.message },
46 });
47
48 res.status(401).json({ error: 'Invalid credentials' });
49 }
50 });Alerting on Suspicious Activity:
1 // Alert on patterns that indicate attacks
2 const ALERT_THRESHOLDS = {
3 failedLoginsPerIp: 10, // per 15 minutes
4 failedLoginsPerUser: 5, // per 15 minutes
5 unauthorizedAccessAttempts: 3, // per hour
6 unusualTransactionPatterns: 5, // per day
7 };
8
9 async function checkAlertThresholds() {
10 const recentFailedLogins = await countRecentEvents('LOGIN_FAILURE', '15m');
11
12 if (recentFailedLogins > ALERT_THRESHOLDS.failedLoginsPerIp) {
13 await sendAlert({
14 severity: 'high',
15 type: 'brute_force_detected',
16 details: { count: recentFailedLogins, window: '15m' },
17 });
18 }
19 }Error Monitoring:Use a service like Sentry to catch errors without exposing sensitive data:
1 import * as Sentry from '@sentry/node';
2
3 Sentry.init({
4 dsn: process.env.SENTRY_DSN,
5 beforeSend(event) {
6 // Scrub sensitive data
7 if (event.request?.data) {
8 event.request.data = scrubSensitiveFields(event.request.data);
9 }
10 return event;
11 },
12 });Security Checklist for Launch
Before launching your fintech MVP, verify each item:Authentication & Authorization- [ ] Passwords hashed with bcrypt (cost factor ≥ 12)- [ ] Rate limiting on login endpoints (max 5 attempts/15 min)- [ ] Session tokens expire (max 24 hours)- [ ] Logout invalidates session tokens- [ ] Authorization checks on every protected endpoint- [ ] Admin functions separated and additionally protectedData Protection- [ ] TLS 1.2+ on all endpoints (no HTTP)- [ ] HSTS header enabled- [ ] Sensitive data encrypted at rest- [ ] PII and financial data not in logs- [ ] Database credentials not in code (use secrets manager)- [ ] Backups encrypted and access-controlledInput Validation- [ ] All inputs validated server-side- [ ] Parameterized queries for all database operations- [ ] File uploads validated and sandboxed- [ ] Content-Type headers enforcedLogging & Monitoring- [ ] Authentication events logged- [ ] Transaction activity logged- [ ] Error monitoring configured- [ ] Alerts for suspicious patterns- [ ] Logs retained for compliance periodInfrastructure- [ ] Secrets management (not in environment files)- [ ] Database not publicly accessible- [ ] Firewall rules reviewed- [ ] Dependency vulnerabilities scanned- [ ] Container images from trusted sources
Ongoing Security Practices
Security isn't a one-time effort. Build these practices into your development process:Weekly:- Review security alerts and logs- Update dependencies with known vulnerabilities- Review access grants (who has access to what)Monthly:- Run automated vulnerability scans- Review and rotate secrets/API keys if needed- Check cloud provider security recommendationsQuarterly:- Conduct internal security review- Update threat model for new features- Review and update incident response plan- Test backup restorationAnnually:- External penetration test (required for many compliance frameworks)- Full security policy review- Security awareness training for team- Disaster recovery drillDependency Scanning:
1 # GitHub Actions workflow for dependency scanning
2 name: Security Scan
3 on: [push, pull_request]
4
5 jobs:
6 security:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v4
10
11 - name: Run npm audit
12 run: npm audit --audit-level=high
13
14 - name: Run Snyk scan
15 uses: snyk/actions/node@master
16 env:
17 SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}When to Hire a Security Expert
You can handle MVP security yourself with this framework. But certain situations require professional help:Hire a security consultant when:1. Preparing for SOC 2 or ISO 27001 — These certifications require formal processes and evidence that benefit from expert guidance2. Processing significant transaction volume — Once you're handling >$1M/month, the risk profile justifies professional assessment3. Serving enterprise customers — B2B fintech customers will require security questionnaires and sometimes penetration test reports4. After a security incident — External expertise helps with root cause analysis and remediation5. Before major funding rounds — Investors (especially in fintech) will ask about security practicesWhat to expect from a security assessment:| Service | Cost Range | Duration | When to Get ||---------|------------|----------|-------------|| Vulnerability scan | $500-2K | 1-2 days | Pre-launch || Penetration test | $10K-50K | 1-4 weeks | Series A+ || Security audit | $20K-100K | 2-8 weeks | SOC 2 prep || vCISO (fractional) | $5K-15K/mo | Ongoing | Series B+ |Building internal security capability:Until you can hire a dedicated security person:1. Designate a security champion on the engineering team2. Include security review in code review process3. Subscribe to security advisories for your stack4. Join fintech security communities for knowledge sharingSecurity is a journey, not a destination. This one-week sprint gives you a solid foundation—but building a security-conscious culture is what protects you long-term.
We help teams design and ship production-grade software in eLearning, fintech, and AI. Let's talk about your project.
Book a call