st, _res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return next(new Error('Missing or malformed authorization header'));
}
try {
const decoded = jwt.verify(authHeader.split(' ')[1], process.env.JWT_ACCESS_SECRET!, {
issuer: 'commerce-platform',
}) as CommerceUser;
req.user = decoded;
next();
} catch {
next(new Error('Invalid or expired access token'));
}
};
export const hashCredential = async (plain: string): Promise<string> => {
return argon2.hash(plain, { type: argon2.argon2id, memoryCost: 65536, parallelism: 4 });
};
**Architecture Rationale:** Argon2id provides memory-hard hashing that resists GPU/ASIC brute-force attacks. Short-lived JWTs (15 minutes) limit the window of token misuse. Separating access and refresh secrets enables independent rotation without invalidating active sessions.
### 2. Traffic Shaping & Bot Defense
In-memory rate limiters fail in clustered deployments. Distributed commerce platforms require sliding-window algorithms backed by external state stores.
```typescript
import { Redis } from 'ioredis';
import { Request, Response, NextFunction } from 'express';
const redis = new Redis(process.env.REDIS_URL!);
export const createSlidingWindowLimiter = (
keyPrefix: string,
maxRequests: number,
windowSeconds: number
) => {
return async (req: Request, res: Response, next: NextFunction) => {
const identifier = req.ip || req.headers['x-forwarded-for'] as string;
const key = `${keyPrefix}:${identifier}`;
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart);
pipeline.zadd(key, now, `${now}-${Math.random()}`);
pipeline.zcard(key);
pipeline.expire(key, windowSeconds);
const results = await pipeline.exec();
const currentCount = results?.[2]?.[1] as number;
if (currentCount > maxRequests) {
res.set('Retry-After', String(windowSeconds));
return res.status(429).json({ error: 'Rate limit exceeded' });
}
next();
};
};
Architecture Rationale: Sorted sets in Redis enable precise sliding windows without edge-case spikes. Storing timestamps allows accurate request counting across multiple application instances. The Retry-After header provides client-side backpressure, reducing unnecessary retry storms.
3. Data Boundary Enforcement
Input validation must occur at the network edge before reaching business logic. Runtime schema enforcement prevents SQL injection, XSS, and type confusion attacks.
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
export const checkoutPayloadSchema = z.object({
cartId: z.string().uuid(),
shippingAddress: z.object({
street: z.string().min(5).max(120),
city: z.string().min(2).max(60),
postalCode: z.string().regex(/^\d{5}(-\d{4})?$/),
country: z.string().length(2),
}),
paymentMethodId: z.string().min(10),
currency: z.enum(['USD', 'EUR', 'GBP']),
});
export const validateCheckout = (req: Request, _res: Response, next: NextFunction) => {
const result = checkoutPayloadSchema.safeParse(req.body);
if (!result.success) {
return next(new Error(`Validation failed: ${result.error.issues[0].message}`));
}
req.validatedBody = result.data;
next();
};
Architecture Rationale: Zod provides compile-time type inference alongside runtime validation. Centralizing schemas ensures consistency across API routes and frontend clients. Rejecting malformed payloads early prevents database query corruption and reduces attack surface.
4. Payment Isolation & Verification
Never handle raw payment credentials. Delegate to PCI-DSS compliant processors and verify incoming webhook events cryptographically.
import crypto from 'crypto';
export const verifyPaymentWebhook = (
payload: string,
signature: string,
secret: string
): boolean => {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
};
Architecture Rationale: HMAC-SHA256 with timing-safe comparison prevents signature forgery and timing attacks. Isolating payment verification into a dedicated service ensures PCI scope reduction. Audit logs should record webhook payloads, verification results, and processing outcomes without storing sensitive card data.
5. Continuous Verification Pipeline
Security cannot be static. Integrate static analysis, dependency scanning, and penetration testing into the delivery pipeline.
# .github/workflows/security-gate.yml
name: Commerce Security Gate
on: [pull_request]
jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high --json > audit-report.json
- uses: github/codeql-action/upload-sarif@v3
with: sarif_file: audit-report.json
sast-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx semgrep scan --config=auto --sarif --output=semgrep.sarif
- uses: github/codeql-action/upload-sarif@v3
with: sarif_file: semgrep.sarif
Architecture Rationale: Automated gates prevent vulnerable dependencies and insecure patterns from reaching production. Semgrep provides pattern-matching analysis for custom business logic flaws. Uploading SARIF files integrates findings directly into pull request reviews, shifting security left without blocking developer velocity.
Pitfall Guide
| Pitfall | Explanation | Fix |
|---|
| Long-lived JWTs without rotation | Tokens valid for days or weeks increase exposure if leaked. Attackers can replay them indefinitely. | Enforce 15-minute access tokens with refresh rotation. Implement token revocation lists in Redis for forced logout. |
| In-memory rate limiting in clusters | Local counters don't synchronize across instances. Attackers distribute requests across nodes to bypass limits. | Use distributed sliding windows backed by Redis or Memcached. Share state via consistent hashing. |
| Client-side validation only | Browser checks can be bypassed with raw HTTP requests. Malicious payloads reach the database directly. | Validate all inputs server-side using strict schemas. Treat client validation as UX enhancement, not security. |
| Storing payment metadata with PII | Mixing transaction logs with customer profiles expands PCI scope and increases breach impact. | Isolate payment data in a dedicated, encrypted vault. Reference via opaque tokens. Maintain separate audit trails. |
| Overly permissive CSP headers | Wildcard sources (*) or unsafe-inline defeat content security policies, enabling XSS injection. | Use strict default-src 'self', whitelist specific CDNs, and implement nonce-based script loading. |
| Ignoring webhook replay attacks | Attackers capture valid webhook payloads and resend them to trigger duplicate charges or state changes. | Track event IDs in a deduplication store. Reject requests with previously processed identifiers. |
| Static dependency scanning without SBOM | npm audit catches known CVEs but misses transitive vulnerabilities and license compliance gaps. | Generate Software Bill of Materials (SBOM) on every build. Integrate with dependency tracking dashboards for runtime visibility. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup MVP (<10k MAU) | In-memory rate limiting + basic JWT auth | Faster iteration, lower infra overhead | +5% infra cost |
| Multi-tenant SaaS Commerce | Distributed Redis rate limiting + tenant-scoped JWTs | Prevents cross-tenant data leakage, scales horizontally | +18% infra cost |
| High-Volume Flash Sales | Token bucket algorithm + CDN-level bot filtering | Handles traffic spikes without application-layer bottlenecks | +25% infra cost |
| Enterprise PCI-DSS L1 | Dedicated payment vault + isolated webhook verifiers | Minimizes audit scope, ensures regulatory compliance | +30% infra cost |
Configuration Template
// src/middleware/security-pipeline.ts
import { Router } from 'express';
import { verifyAccess } from './auth/verify-access';
import { createSlidingWindowLimiter } from './traffic/rate-limiter';
import { validateCheckout } from './validation/checkout-schema';
import { verifyPaymentWebhook } from './payments/webhook-verifier';
const commerceRouter = Router();
// Global security headers
commerceRouter.use((_req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://cdn.trusted-provider.com; style-src 'self' 'unsafe-inline'"
);
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
next();
});
// Protected commerce routes
commerceRouter.post(
'/api/v1/checkout',
verifyAccess,
createSlidingWindowLimiter('checkout', 10, 60),
validateCheckout,
async (req, res) => {
// Business logic executes only after all guards pass
res.json({ status: 'processing', orderId: req.validatedBody.cartId });
}
);
// Webhook endpoint (no auth header, relies on signature)
commerceRouter.post(
'/api/v1/payments/webhook',
createSlidingWindowLimiter('webhook', 50, 60),
async (req, res) => {
const signature = req.headers['x-payment-signature'] as string;
const isValid = verifyPaymentWebhook(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
// Process verified event
res.status(200).json({ received: true });
}
);
export { commerceRouter };
Quick Start Guide
- Initialize security dependencies: Run
npm install argon2 jsonwebtoken zod ioredis helmet to install cryptographic, validation, and distributed state libraries.
- Configure environment variables: Set
JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, REDIS_URL, and WEBHOOK_SECRET in your deployment environment. Never commit secrets to version control.
- Attach middleware pipeline: Import the security router into your Express/Fastify application. Ensure it mounts before any business logic routes.
- Enable CI/CD gates: Add Semgrep and
npm audit steps to your pull request workflow. Block merges when high-severity findings are detected.
- Validate with test payloads: Use tools like
curl or Postman to send malformed requests, expired tokens, and invalid webhook signatures. Confirm that all guards reject unauthorized traffic before reaching controllers.