Overview
This vulnerability occurs when an application’s business logic contains flaws that allow attackers to manipulate the intended flow of operations. Unlike technical vulnerabilities, these are design-level issues where the application works as coded but the logic itself is flawed. Common examples include bypassing payment processes, manipulating shopping cart prices, circumventing rate limits, exploiting race conditions in financial transactions, or abusing workflow sequences to gain unauthorized privileges. 🎯💼Business Impact
Business logic flaws can have severe financial and operational consequences.- Financial Loss: Direct monetary losses through price manipulation, bypassed payment flows, or fraudulent transactions.
- Inventory Manipulation: Purchasing items at incorrect prices, claiming excessive discounts, or manipulating stock levels.
- Privilege Escalation: Bypassing approval workflows or role-based restrictions to gain unauthorized access or capabilities.
- Data Integrity Issues: Creating inconsistent states in the database through race conditions or improper state transitions.
Reference Details
CWE ID: CWE-840
OWASP Top 10 (2021): A04:2021 - Insecure Design
Severity: Medium to Critical (depending on the business impact)
Framework-Specific Analysis and Remediation
Business logic vulnerabilities are primarily design and implementation issues rather than configuration problems. They require careful analysis of application workflows, state management, and transaction processing. The fix involves:- State Validation: Verify the application is in the correct state before allowing operations.
- Atomic Operations: Use database transactions and locks to prevent race conditions.
- Server-Side Validation: Never trust client-side data for critical business decisions.
- Workflow Enforcement: Ensure operations can only occur in the intended sequence.
- Python
- Java
- .NET(C#)
- PHP
- Node.js
- Ruby
Framework Context
Common in Django and Flask applications handling e-commerce, financial transactions, or multi-step workflows. Issues often arise in views handling cart operations, payment processing, or user role transitions.Vulnerable Scenario 1: Price Manipulation in Shopping Cart
Copy
# views.py (Django E-commerce - VULNERABLE)
from django.views import View
from django.http import JsonResponse
import json
class AddToCartView(View):
def post(self, request):
data = json.loads(request.body)
product_id = data.get('product_id')
quantity = data.get('quantity')
# DANGEROUS: Accepting price from client
price = data.get('price') # Client can manipulate this!
# Add to cart with client-provided price
cart_item = CartItem.objects.create(
user=request.user,
product_id=product_id,
quantity=quantity,
price=price # Using client-provided price
)
return JsonResponse({'status': 'success'})
Vulnerable Scenario 2: Race Condition in Balance Transfer
Copy
# views.py (Flask Banking App - VULNERABLE)
@app.route('/transfer', methods=['POST'])
@login_required
def transfer_funds():
amount = float(request.form['amount'])
recipient_id = request.form['recipient_id']
sender = User.query.get(current_user.id)
recipient = User.query.get(recipient_id)
# DANGEROUS: Non-atomic check and update
if sender.balance >= amount: # Check
time.sleep(0.1) # Simulating processing delay
sender.balance -= amount # Update (race condition window)
recipient.balance += amount
db.session.commit()
return jsonify({'status': 'success'})
return jsonify({'error': 'Insufficient funds'}), 400
Mitigation and Best Practices
- Server-Side Price Validation: Always fetch prices from the database based on product ID.
- Database Transactions: Use atomic operations with proper locking mechanisms.
- State Machines: Implement proper state transitions for multi-step processes.
- Idempotency: Ensure operations can’t be repeated to cause unintended effects.
Secure Code Example
Copy
# views.py (Django E-commerce - SECURE)
from django.db import transaction
from django.views import View
from decimal import Decimal
class AddToCartView(View):
def post(self, request):
data = json.loads(request.body)
product_id = data.get('product_id')
quantity = int(data.get('quantity', 1))
# SECURE: Fetch price from database, not client
try:
product = Product.objects.select_for_update().get(id=product_id)
except Product.DoesNotExist:
return JsonResponse({'error': 'Product not found'}, status=404)
# SECURE: Validate business rules
if quantity <= 0 or quantity > product.max_per_order:
return JsonResponse({'error': 'Invalid quantity'}, status=400)
if product.stock < quantity:
return JsonResponse({'error': 'Insufficient stock'}, status=400)
# SECURE: Use server-side price
cart_item = CartItem.objects.create(
user=request.user,
product=product,
quantity=quantity,
price=product.current_price # Server-controlled price
)
return JsonResponse({'status': 'success'})
# views.py (Flask Banking - SECURE)
@app.route('/transfer', methods=['POST'])
@login_required
def transfer_funds():
amount = Decimal(request.form['amount'])
recipient_id = request.form['recipient_id']
# SECURE: Validate amount
if amount <= 0 or amount > Decimal('10000'):
return jsonify({'error': 'Invalid amount'}), 400
# SECURE: Use database transaction with row locking
with db.session.begin():
sender = User.query.with_for_update().get(current_user.id)
recipient = User.query.with_for_update().get(recipient_id)
if not recipient:
return jsonify({'error': 'Recipient not found'}), 404
if sender.balance < amount:
return jsonify({'error': 'Insufficient funds'}), 400
# SECURE: Atomic update within transaction
sender.balance -= amount
recipient.balance += amount
# Log transaction for audit
Transaction.create(
sender_id=sender.id,
recipient_id=recipient.id,
amount=amount,
timestamp=datetime.utcnow()
)
return jsonify({'status': 'success'})
Testing Strategy
Test with concurrent requests to identify race conditions. Attempt to manipulate client-side values (prices, quantities, IDs). Verify state transitions follow the intended workflow. Use tools like Locust or Apache JMeter for load testing concurrent operations.Framework Context
Common in Spring Boot applications handling financial transactions, order processing, or approval workflows. Issues often arise in REST controllers and service layers.Vulnerable Scenario 1: Discount Code Abuse
Copy
// OrderController.java (VULNERABLE)
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping("/apply-discount")
public ResponseEntity<?> applyDiscount(@RequestBody DiscountRequest request) {
Order order = orderRepository.findById(request.getOrderId()).orElseThrow();
DiscountCode discount = discountRepository.findByCode(request.getCode()).orElseThrow();
// DANGEROUS: No check if discount was already applied
double discountAmount = order.getTotal() * discount.getPercentage();
order.setTotal(order.getTotal() - discountAmount);
// DANGEROUS: No check for discount validity or usage limits
orderRepository.save(order);
return ResponseEntity.ok(order);
}
}
Vulnerable Scenario 2: Workflow State Bypass
Copy
// DocumentService.java (VULNERABLE)
@Service
public class DocumentService {
public void approveDocument(Long documentId, Long userId) {
Document doc = documentRepository.findById(documentId).orElseThrow();
User user = userRepository.findById(userId).orElseThrow();
// DANGEROUS: Not checking current state or user permissions
doc.setStatus("APPROVED");
doc.setApprovedBy(user);
documentRepository.save(doc);
// User could approve their own document or skip review stages
}
}
Mitigation and Best Practices
- State Validation: Always verify the current state before transitions.
- Pessimistic Locking: Use
@Lock(LockModeType.PESSIMISTIC_WRITE)for critical operations. - Business Rule Engine: Consider using a rules engine for complex business logic.
- Audit Logging: Track all state changes and critical operations.
Secure Code Example
Copy
// OrderController.java (SECURE)
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping("/apply-discount")
@Transactional
public ResponseEntity<?> applyDiscount(@RequestBody DiscountRequest request,
Authentication auth) {
// SECURE: Lock order to prevent concurrent modifications
Order order = orderRepository.findByIdWithLock(request.getOrderId())
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
// SECURE: Verify order belongs to user
if (!order.getUserId().equals(auth.getName())) {
throw new AccessDeniedException("Order access denied");
}
// SECURE: Check order state
if (!order.getStatus().equals("PENDING")) {
throw new BusinessLogicException("Cannot apply discount to processed order");
}
DiscountCode discount = discountRepository.findByCode(request.getCode())
.orElseThrow(() -> new ResourceNotFoundException("Invalid discount code"));
// SECURE: Validate discount constraints
if (discount.getExpiryDate().isBefore(LocalDate.now())) {
throw new BusinessLogicException("Discount code expired");
}
// SECURE: Check if already applied
if (order.getAppliedDiscounts().contains(discount.getCode())) {
throw new BusinessLogicException("Discount already applied");
}
// SECURE: Check usage limits
long usageCount = orderRepository.countByAppliedDiscountsContaining(discount.getCode());
if (usageCount >= discount.getMaxUsage()) {
throw new BusinessLogicException("Discount usage limit reached");
}
// SECURE: Apply discount with validation
BigDecimal discountAmount = order.getTotal()
.multiply(BigDecimal.valueOf(discount.getPercentage()))
.setScale(2, RoundingMode.HALF_UP);
if (discountAmount.compareTo(discount.getMaxDiscountAmount()) > 0) {
discountAmount = discount.getMaxDiscountAmount();
}
order.setTotal(order.getTotal().subtract(discountAmount));
order.getAppliedDiscounts().add(discount.getCode());
order.setLastModified(Instant.now());
// Audit log
auditService.log("DISCOUNT_APPLIED", order.getId(), auth.getName());
return ResponseEntity.ok(orderRepository.save(order));
}
}
// DocumentService.java (SECURE)
@Service
@Transactional
public class DocumentService {
public void approveDocument(Long documentId, Authentication auth) {
// SECURE: Lock document for update
Document doc = documentRepository.findByIdForUpdate(documentId)
.orElseThrow(() -> new ResourceNotFoundException("Document not found"));
User approver = userRepository.findByUsername(auth.getName())
.orElseThrow();
// SECURE: Validate current state
if (!doc.getStatus().equals("PENDING_APPROVAL")) {
throw new BusinessLogicException("Document not in approvable state");
}
// SECURE: Check permissions
if (!approver.hasRole("APPROVER")) {
throw new AccessDeniedException("User lacks approval permission");
}
// SECURE: Prevent self-approval
if (doc.getCreatedBy().equals(approver.getId())) {
throw new BusinessLogicException("Cannot approve own document");
}
// SECURE: State transition with validation
doc.setStatus("APPROVED");
doc.setApprovedBy(approver);
doc.setApprovalDate(Instant.now());
documentRepository.save(doc);
// Audit trail
auditService.log("DOCUMENT_APPROVED", doc.getId(), approver.getUsername());
}
}
Testing Strategy
Create test cases for edge conditions: negative quantities, extreme values, concurrent operations. Test state transitions with invalid starting states. Verify that operations respect business constraints. Use@DirtiesContext in Spring tests to ensure clean state between tests.Framework Context
Common in ASP.NET Core applications handling e-commerce, banking, or multi-step wizards. Issues often arise in controllers and service layers handling transactions.Vulnerable Scenario 1: Reward Points Manipulation
Copy
// RewardsController.cs (VULNERABLE)
[ApiController]
[Route("api/[controller]")]
public class RewardsController : ControllerBase
{
[HttpPost("redeem")]
public async Task<IActionResult> RedeemPoints([FromBody] RedeemRequest request)
{
var user = await _userService.GetUserAsync(User.Identity.Name);
// DANGEROUS: No validation of points value from client
var pointsToRedeem = request.Points;
// DANGEROUS: Simple subtraction without checks
user.RewardPoints -= pointsToRedeem;
var creditAmount = pointsToRedeem * 0.01m; // $0.01 per point
user.AccountCredit += creditAmount;
await _userService.UpdateUserAsync(user);
return Ok(new { Credit = creditAmount });
}
}
Vulnerable Scenario 2: Concurrent Booking Race Condition
Copy
// BookingService.cs (VULNERABLE)
public class BookingService
{
public async Task<BookingResult> BookSeat(int seatId, string userId)
{
var seat = await _context.Seats.FindAsync(seatId);
// DANGEROUS: Check-then-act pattern vulnerable to race conditions
if (seat.IsAvailable)
{
await Task.Delay(100); // Simulating processing
seat.IsAvailable = false;
seat.BookedBy = userId;
seat.BookingTime = DateTime.UtcNow;
await _context.SaveChangesAsync();
return new BookingResult { Success = true };
}
return new BookingResult { Success = false };
}
}
Mitigation and Best Practices
- Optimistic/Pessimistic Concurrency: Use EF Core’s concurrency tokens or row versioning.
- Domain-Driven Design: Encapsulate business rules in domain models.
- Saga Pattern: For distributed transactions across multiple services.
- Input Validation: Always validate and sanitize user inputs server-side.
Secure Code Example
Copy
// RewardsController.cs (SECURE)
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class RewardsController : ControllerBase
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
private readonly ILogger<RewardsController> _logger;
[HttpPost("redeem")]
public async Task<IActionResult> RedeemPoints([FromBody] RedeemRequest request)
{
// SECURE: Validate input
if (request.Points <= 0 || request.Points > 10000)
{
return BadRequest("Invalid points value");
}
using var context = _contextFactory.CreateDbContext();
using var transaction = await context.Database.BeginTransactionAsync();
try
{
// SECURE: Use row locking
var user = await context.Users
.FromSqlRaw("SELECT * FROM Users WITH (UPDLOCK) WHERE Id = {0}", User.Identity.Name)
.FirstOrDefaultAsync();
if (user == null)
{
return NotFound("User not found");
}
// SECURE: Validate business rules
if (user.RewardPoints < request.Points)
{
return BadRequest("Insufficient reward points");
}
// SECURE: Check daily redemption limit
var todayRedemptions = await context.RewardRedemptions
.Where(r => r.UserId == user.Id && r.Date == DateTime.Today)
.SumAsync(r => r.Points);
if (todayRedemptions + request.Points > 5000)
{
return BadRequest("Daily redemption limit exceeded");
}
// SECURE: Atomic operation with audit trail
user.RewardPoints -= request.Points;
var creditAmount = request.Points * 0.01m;
user.AccountCredit += creditAmount;
var redemption = new RewardRedemption
{
UserId = user.Id,
Points = request.Points,
CreditAmount = creditAmount,
Date = DateTime.Today,
Timestamp = DateTime.UtcNow
};
context.RewardRedemptions.Add(redemption);
await context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInformation("User {UserId} redeemed {Points} points for ${Credit}",
user.Id, request.Points, creditAmount);
return Ok(new { Credit = creditAmount, RemainingPoints = user.RewardPoints });
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Error redeeming points for user {UserId}", User.Identity.Name);
return StatusCode(500, "An error occurred");
}
}
}
// BookingService.cs (SECURE)
public class BookingService
{
private readonly AppDbContext _context;
private readonly ILogger<BookingService> _logger;
public async Task<BookingResult> BookSeat(int seatId, string userId)
{
// SECURE: Use transaction with appropriate isolation level
using var transaction = await _context.Database
.BeginTransactionAsync(IsolationLevel.Serializable);
try
{
// SECURE: Lock the row for update
var seat = await _context.Seats
.FromSqlRaw("SELECT * FROM Seats WITH (UPDLOCK) WHERE Id = {0}", seatId)
.FirstOrDefaultAsync();
if (seat == null)
{
return new BookingResult { Success = false, Message = "Seat not found" };
}
// SECURE: Validate state within transaction
if (!seat.IsAvailable)
{
return new BookingResult { Success = false, Message = "Seat already booked" };
}
// SECURE: Additional business rule validation
if (seat.EventDate < DateTime.UtcNow.AddHours(1))
{
return new BookingResult { Success = false, Message = "Booking closed" };
}
// SECURE: Check user's concurrent bookings
var userBookingsCount = await _context.Seats
.CountAsync(s => s.BookedBy == userId && s.EventId == seat.EventId);
if (userBookingsCount >= 4)
{
return new BookingResult { Success = false, Message = "Maximum seats per user exceeded" };
}
// SECURE: Update with concurrency token
seat.IsAvailable = false;
seat.BookedBy = userId;
seat.BookingTime = DateTime.UtcNow;
seat.BookingReference = Guid.NewGuid().ToString();
// Use optimistic concurrency with RowVersion
_context.Entry(seat).Property("RowVersion").OriginalValue = seat.RowVersion;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInformation("Seat {SeatId} booked by {UserId}", seatId, userId);
return new BookingResult
{
Success = true,
BookingReference = seat.BookingReference
};
}
catch (DbUpdateConcurrencyException)
{
await transaction.RollbackAsync();
return new BookingResult { Success = false, Message = "Seat no longer available" };
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Booking failed for seat {SeatId}", seatId);
throw;
}
}
}
Testing Strategy
Use xUnit with concurrent test execution to identify race conditions. Create integration tests that simulate multiple users performing the same operation simultaneously. Test boundary conditions and negative scenarios. UseTransactionScope in tests to ensure database rollback.Framework Context
Common in Laravel applications handling e-commerce, subscription services, or content management. Issues often arise in controllers handling payments, user upgrades, or content access.Vulnerable Scenario 1: Subscription Upgrade Bypass
Copy
// SubscriptionController.php (Laravel - VULNERABLE)
class SubscriptionController extends Controller
{
public function upgrade(Request $request)
{
$user = Auth::user();
$planId = $request->input('plan_id');
// DANGEROUS: No verification of payment
$user->subscription_plan_id = $planId;
$user->subscription_expires = now()->addMonth();
$user->save();
return response()->json(['message' => 'Subscription upgraded']);
}
}
Vulnerable Scenario 2: Voucher Generation Abuse
Copy
// VoucherController.php (VULNERABLE)
class VoucherController extends Controller
{
public function generateVoucher(Request $request)
{
$amount = $request->input('amount');
$quantity = $request->input('quantity');
// DANGEROUS: No validation or rate limiting
for ($i = 0; $i < $quantity; $i++) {
Voucher::create([
'code' => Str::random(10),
'amount' => $amount,
'created_by' => Auth::id(),
'expires_at' => now()->addYear()
]);
}
return response()->json(['message' => "$quantity vouchers created"]);
}
}
Mitigation and Best Practices
- Payment Verification: Always verify payment status before granting access.
- Database Transactions: Use Laravel’s DB transactions for atomic operations.
- Rate Limiting: Implement throttling for sensitive operations.
- Authorization Gates: Use Laravel’s authorization features for access control.
Secure Code Example
Copy
// SubscriptionController.php (Laravel - SECURE)
class SubscriptionController extends Controller
{
public function upgrade(Request $request)
{
$validated = $request->validate([
'plan_id' => 'required|exists:subscription_plans,id',
'payment_token' => 'required|string'
]);
$user = Auth::user();
$plan = SubscriptionPlan::findOrFail($validated['plan_id']);
// SECURE: Start database transaction
return DB::transaction(function () use ($user, $plan, $validated) {
// SECURE: Lock user record
$user = User::where('id', $user->id)->lockForUpdate()->first();
// SECURE: Validate current subscription state
if ($user->subscription_plan_id >= $plan->id) {
throw new BusinessLogicException('Cannot downgrade subscription');
}
// SECURE: Calculate prorated amount
$amount = $this->calculateProratedAmount($user, $plan);
// SECURE: Process payment first
try {
$payment = PaymentGateway::charge([
'token' => $validated['payment_token'],
'amount' => $amount,
'description' => "Upgrade to {$plan->name}"
]);
} catch (PaymentException $e) {
Log::error('Payment failed', ['user' => $user->id, 'error' => $e->getMessage()]);
throw new BusinessLogicException('Payment processing failed');
}
// SECURE: Only upgrade after successful payment
$previousPlan = $user->subscription_plan_id;
$user->subscription_plan_id = $plan->id;
$user->subscription_expires = $user->subscription_expires
? $user->subscription_expires->addMonth()
: now()->addMonth();
$user->save();
// SECURE: Create audit record
SubscriptionHistory::create([
'user_id' => $user->id,
'from_plan_id' => $previousPlan,
'to_plan_id' => $plan->id,
'payment_id' => $payment->id,
'amount' => $amount,
'upgraded_at' => now()
]);
return response()->json([
'message' => 'Subscription upgraded successfully',
'expires_at' => $user->subscription_expires
]);
});
}
private function calculateProratedAmount($user, $newPlan)
{
// SECURE: Complex business logic for proration
if (!$user->subscription_expires || $user->subscription_expires->isPast()) {
return $newPlan->price;
}
$daysRemaining = now()->diffInDays($user->subscription_expires);
$currentPlan = SubscriptionPlan::find($user->subscription_plan_id);
$dailyCredit = $currentPlan ? ($currentPlan->price / 30) * $daysRemaining : 0;
return max(0, $newPlan->price - $dailyCredit);
}
}
// VoucherController.php (SECURE)
class VoucherController extends Controller
{
public function __construct()
{
// SECURE: Rate limiting
$this->middleware('throttle:5,1')->only('generateVoucher');
}
public function generateVoucher(Request $request)
{
// SECURE: Validate input
$validated = $request->validate([
'amount' => 'required|numeric|min:1|max:100',
'quantity' => 'required|integer|min:1|max:10',
'reason' => 'required|string|max:255'
]);
// SECURE: Authorization check
if (!Auth::user()->can('create-vouchers')) {
abort(403, 'Unauthorized to create vouchers');
}
// SECURE: Check daily limits
$todayCount = Voucher::where('created_by', Auth::id())
->whereDate('created_at', today())
->count();
if ($todayCount + $validated['quantity'] > 50) {
throw new BusinessLogicException('Daily voucher limit exceeded');
}
// SECURE: Use transaction for atomic operation
$vouchers = DB::transaction(function () use ($validated) {
$vouchers = [];
for ($i = 0; $i < $validated['quantity']; $i++) {
// SECURE: Generate cryptographically secure codes
$code = strtoupper(Str::random(8));
// SECURE: Ensure uniqueness
while (Voucher::where('code', $code)->exists()) {
$code = strtoupper(Str::random(8));
}
$voucher = Voucher::create([
'code' => $code,
'amount' => $validated['amount'],
'created_by' => Auth::id(),
'reason' => $validated['reason'],
'expires_at' => now()->addDays(30), // SECURE: Reasonable expiry
'max_uses' => 1 // SECURE: Single use
]);
$vouchers[] = $voucher;
// SECURE: Audit log
activity()
->causedBy(Auth::user())
->performedOn($voucher)
->withProperties(['amount' => $validated['amount'], 'reason' => $validated['reason']])
->log('Voucher generated');
}
return $vouchers;
});
return response()->json([
'message' => count($vouchers) . ' vouchers created',
'vouchers' => $vouchers->pluck('code')
]);
}
}
Testing Strategy
Use PHPUnit with database transactions for testing. Create tests for concurrent operations using multiple database connections. Test with invalid state transitions and boundary values. Use Laravel Dusk for end-to-end testing of multi-step workflows.Framework Context
Common in Express applications handling real-time operations, gaming mechanics, or API rate limiting. Issues often arise in middleware and route handlers dealing with session management or resource allocation.Vulnerable Scenario 1: API Rate Limit Bypass
Copy
// rateLimiter.js (VULNERABLE)
const rateLimitMap = new Map();
const rateLimiter = (req, res, next) => {
const userId = req.user?.id;
// DANGEROUS: Only checking authenticated users
if (!userId) {
return next(); // Unauthenticated users bypass rate limiting!
}
const userLimits = rateLimitMap.get(userId) || { count: 0, resetTime: Date.now() + 60000 };
// DANGEROUS: Client can manipulate headers to reset counter
if (req.headers['x-reset-limit'] === 'true') {
rateLimitMap.delete(userId);
return next();
}
if (userLimits.count >= 100) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
userLimits.count++;
rateLimitMap.set(userId, userLimits);
next();
};
Vulnerable Scenario 2: Game Score Manipulation
Copy
// gameController.js (VULNERABLE)
router.post('/submit-score', async (req, res) => {
const { gameId, score, timeSpent } = req.body;
const userId = req.user.id;
// DANGEROUS: Trusting client-provided score
const gameSession = await GameSession.findOne({ userId, gameId });
// DANGEROUS: No validation of score legitimacy
gameSession.score = score;
gameSession.timeSpent = timeSpent;
gameSession.completed = true;
await gameSession.save();
// Award prizes based on score
if (score > 10000) {
await User.findByIdAndUpdate(userId, { $inc: { coins: 1000 } });
}
res.json({ success: true });
});
Mitigation and Best Practices
- Server-Side State: Maintain game state and calculations server-side.
- Distributed Locking: Use Redis for distributed rate limiting.
- Cryptographic Signatures: Sign critical values to prevent tampering.
- Session Validation: Verify session integrity and timing constraints.
Secure Code Example
Copy
// rateLimiter.js (SECURE)
const Redis = require('ioredis');
const redis = new Redis();
const crypto = require('crypto');
const rateLimiter = (options = {}) => {
const {
windowMs = 60000,
max = 100,
keyGenerator = (req) => req.ip // Default to IP-based
} = options;
return async (req, res, next) => {
// SECURE: Generate rate limit key
const key = `rate_limit:${keyGenerator(req)}`;
try {
// SECURE: Use Redis for distributed rate limiting
const pipeline = redis.pipeline();
const now = Date.now();
const window = now - windowMs;
// Remove old entries
pipeline.zremrangebyscore(key, 0, window);
// Add current request
pipeline.zadd(key, now, `${now}-${crypto.randomBytes(4).toString('hex')}`);
// Count requests in window
pipeline.zcard(key);
// Set expiry
pipeline.expire(key, Math.ceil(windowMs / 1000));
const results = await pipeline.exec();
const requestCount = results[2][1];
// SECURE: Set rate limit headers
res.setHeader('X-RateLimit-Limit', max);
res.setHeader('X-RateLimit-Remaining', Math.max(0, max - requestCount));
res.setHeader('X-RateLimit-Reset', new Date(now + windowMs).toISOString());
if (requestCount > max) {
// SECURE: Log potential abuse
logger.warn('Rate limit exceeded', {
key,
count: requestCount,
ip: req.ip,
user: req.user?.id
});
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(windowMs / 1000)
});
}
next();
} catch (error) {
logger.error('Rate limiter error', error);
// Fail open but log the issue
next();
}
};
};
// gameController.js (SECURE)
const crypto = require('crypto');
const gameSecret = process.env.GAME_SECRET;
router.post('/start-game', async (req, res) => {
const { gameId } = req.body;
const userId = req.user.id;
// SECURE: Create server-side game session
const session = await GameSession.create({
userId,
gameId,
startTime: Date.now(),
sessionToken: crypto.randomBytes(32).toString('hex'),
maxScore: 0,
moves: []
});
// SECURE: Send session token (not the actual game state)
res.json({
sessionId: session._id,
sessionToken: session.sessionToken
});
});
router.post('/game-move', async (req, res) => {
const { sessionId, sessionToken, move } = req.body;
// SECURE: Validate session
const session = await GameSession.findById(sessionId);
if (!session || session.sessionToken !== sessionToken) {
return res.status(401).json({ error: 'Invalid session' });
}
// SECURE: Check session timeout
if (Date.now() - session.startTime > 3600000) { // 1 hour limit
session.completed = true;
session.timedOut = true;
await session.save();
return res.status(400).json({ error: 'Session expired' });
}
// SECURE: Validate move server-side
const moveValidation = validateMove(session, move);
if (!moveValidation.valid) {
// Log potential cheating attempt
logger.warn('Invalid move attempted', {
userId: session.userId,
sessionId,
move
});
return res.status(400).json({ error: 'Invalid move' });
}
// SECURE: Calculate score server-side
const scoreIncrement = calculateScore(move, session.moves.length);
session.moves.push({
move,
timestamp: Date.now(),
score: scoreIncrement
});
session.maxScore += scoreIncrement;
// SECURE: Check for impossible scores
const theoreticalMax = calculateTheoreticalMax(session.moves.length, Date.now() - session.startTime);
if (session.maxScore > theoreticalMax) {
session.flaggedForReview = true;
logger.alert('Suspicious game activity', {
userId: session.userId,
sessionId,
score: session.maxScore,
theoretical: theoreticalMax
});
}
await session.save();
res.json({
currentScore: session.maxScore,
moveAccepted: true
});
});
router.post('/end-game', async (req, res) => {
const { sessionId, sessionToken } = req.body;
// SECURE: Lock session for completion
const session = await GameSession.findOneAndUpdate(
{
_id: sessionId,
sessionToken,
completed: false
},
{
$set: {
completed: true,
endTime: Date.now()
}
},
{
new: true,
runValidators: true
}
);
if (!session) {
return res.status(400).json({ error: 'Invalid or already completed session' });
}
// SECURE: Validate final score
const calculatedScore = session.moves.reduce((sum, m) => sum + m.score, 0);
if (calculatedScore !== session.maxScore) {
logger.alert('Score mismatch detected', {
sessionId,
calculated: calculatedScore,
stored: session.maxScore
});
session.flaggedForReview = true;
await session.save();
return res.status(400).json({ error: 'Score validation failed' });
}
// SECURE: Award prizes only for validated scores
if (!session.flaggedForReview && session.maxScore > 10000) {
await User.findByIdAndUpdate(
session.userId,
{
$inc: { coins: Math.min(1000, Math.floor(session.maxScore / 100)) },
$push: {
achievements: {
type: 'HIGH_SCORE',
score: session.maxScore,
date: new Date()
}
}
},
{ runValidators: true }
);
}
res.json({
finalScore: session.maxScore,
coinsEarned: session.flaggedForReview ? 0 : Math.min(1000, Math.floor(session.maxScore / 100))
});
});
Testing Strategy
Use Mocha/Chai with sinon for mocking time-based operations. Create stress tests with multiple concurrent requests using supertest. Test with manipulated client data to ensure server-side validation catches cheating attempts. Use Jest for testing race conditions with proper async handling.Framework Context
Common in Rails applications handling inventory management, auction systems, or reservation platforms. Issues often arise in controllers and models dealing with competitive resource allocation.Vulnerable Scenario 1: Inventory Double-Spending
Copy
# products_controller.rb (VULNERABLE)
class ProductsController < ApplicationController
def purchase
product = Product.find(params[:id])
quantity = params[:quantity].to_i
# DANGEROUS: Race condition between check and update
if product.stock >= quantity
product.stock -= quantity
product.save
Order.create!(
user: current_user,
product: product,
quantity: quantity,
total: product.price * quantity
)
render json: { success: true }
else
render json: { error: 'Insufficient stock' }, status: 400
end
end
end
Vulnerable Scenario 2: Auction Bid Manipulation
Copy
# auction.rb (VULNERABLE)
class Auction < ApplicationRecord
def place_bid(user, amount)
# DANGEROUS: No validation of bid increments
if amount > self.current_bid
self.current_bid = amount
self.highest_bidder = user
self.save
# DANGEROUS: Time extension can be abused
if self.ends_at - Time.current < 1.minute
self.ends_at += 5.minutes
self.save
end
true
else
false
end
end
end
Mitigation and Best Practices
- Pessimistic Locking: Use
with_lockfor critical sections. - Optimistic Locking: Add
lock_versionto tables for concurrent updates. - State Machines: Use gems like AASM for controlled state transitions.
- Database Constraints: Add check constraints for business rules.
Secure Code Example
Copy
# products_controller.rb (SECURE)
class ProductsController < ApplicationController
def purchase
quantity = params[:quantity].to_i
# SECURE: Input validation
if quantity <= 0 || quantity > 10
return render json: { error: 'Invalid quantity' }, status: 400
end
# SECURE: Use transaction with locking
order = nil
ActiveRecord::Base.transaction do
product = Product.lock.find(params[:id])
# SECURE: Verify product is purchasable
unless product.available_for_purchase?
raise BusinessLogicError, 'Product not available'
end
# SECURE: Check stock within lock
if product.stock < quantity
raise BusinessLogicError, 'Insufficient stock'
end
# SECURE: Check purchase limits
recent_orders_count = current_user.orders
.where(product: product)
.where('created_at > ?', 24.hours.ago)
.sum(:quantity)
if recent_orders_count + quantity > product.max_per_customer
raise BusinessLogicError, 'Purchase limit exceeded'
end
# SECURE: Update stock
product.stock -= quantity
product.save!
# SECURE: Create order with validation
order = Order.create!(
user: current_user,
product: product,
quantity: quantity,
unit_price: product.current_price, # Store price at time of purchase
total: product.current_price * quantity,
status: 'pending_payment'
)
# SECURE: Reserve stock for limited time
StockReservation.create!(
order: order,
product: product,
quantity: quantity,
expires_at: 15.minutes.from_now
)
end
# Trigger payment processing job
ProcessPaymentJob.perform_later(order)
render json: {
success: true,
order_id: order.id,
reservation_expires_at: order.stock_reservation.expires_at
}
rescue BusinessLogicError => e
render json: { error: e.message }, status: 400
rescue ActiveRecord::RecordInvalid => e
render json: { error: 'Invalid order data' }, status: 400
end
end
# auction.rb (SECURE)
class Auction < ApplicationRecord
include AASM
has_many :bids
belongs_to :highest_bidder, class_name: 'User', optional: true
# SECURE: Use optimistic locking
# Migration: add_column :auctions, :lock_version, :integer, default: 0, null: false
# SECURE: State machine for auction lifecycle
aasm column: 'status' do
state :draft, initial: true
state :active
state :ending
state :completed
state :cancelled
event :activate do
transitions from: :draft, to: :active
after do
self.starts_at = Time.current
self.ends_at = self.starts_at + self.duration.hours
end
end
event :complete do
transitions from: [:active, :ending], to: :completed
end
end
def place_bid(user, amount)
# SECURE: Use transaction with retry on optimistic lock failure
max_retries = 3
retry_count = 0
begin
ActiveRecord::Base.transaction do
# SECURE: Lock the auction record
auction = Auction.lock.find(self.id)
# SECURE: Validate auction state
unless auction.active? || auction.ending?
raise BusinessLogicError, 'Auction is not accepting bids'
end
# SECURE: Validate bid amount
minimum_bid = auction.current_bid.to_f + auction.minimum_increment
if amount < minimum_bid
raise BusinessLogicError, "Bid must be at least #{minimum_bid}"
end
# SECURE: Validate user can bid
if auction.seller_id == user.id
raise BusinessLogicError, 'Cannot bid on own auction'
end
if auction.highest_bidder_id == user.id
raise BusinessLogicError, 'You are already the highest bidder'
end
# SECURE: Check user's bid limit
user_total_bids = user.bids
.joins(:auction)
.where(auctions: { status: [:active, :ending] })
.sum(:amount)
if user_total_bids + amount > user.bid_limit
raise BusinessLogicError, 'Bid limit exceeded'
end
# SECURE: Create bid record
bid = auction.bids.create!(
user: user,
amount: amount,
ip_address: user.current_sign_in_ip,
placed_at: Time.current
)
# SECURE: Update auction
auction.current_bid = amount
auction.highest_bidder = user
auction.bid_count += 1
# SECURE: Controlled time extension with limits
time_remaining = auction.ends_at - Time.current
if time_remaining < 2.minutes && auction.extensions_count < 3
auction.ends_at += 2.minutes
auction.extensions_count += 1
auction.ending! if time_remaining < 5.minutes && auction.active?
end
auction.save!
# Notify previous highest bidder
if auction.previous_changes['highest_bidder_id']
OutbidNotificationJob.perform_later(
auction.previous_changes['highest_bidder_id'][0],
auction
)
end
bid
end
rescue ActiveRecord::StaleObjectError
# SECURE: Handle optimistic locking conflict
retry_count += 1
if retry_count < max_retries
sleep(0.1 * retry_count) # Exponential backoff
retry
else
raise BusinessLogicError, 'Unable to place bid due to high activity'
end
end
end
# SECURE: Scheduled job to finalize auctions
def self.finalize_ended_auctions
Auction.where(status: [:active, :ending])
.where('ends_at < ?', Time.current)
.find_each do |auction|
auction.with_lock do
next unless auction.active? || auction.ending?
auction.complete!
if auction.highest_bidder.present?
# Create purchase obligation
Purchase.create!(
buyer: auction.highest_bidder,
auction: auction,
amount: auction.current_bid,
due_date: 3.days.from_now
)
# Notify winner
AuctionWonNotificationJob.perform_later(auction)
end
end
end
end
end
Testing Strategy
Use RSpec withtransactional fixtures disabled for testing locking behavior. Create concurrent tests using threads or the parallel_tests gem. Test state machines with all possible transitions. Use FactoryBot to create complex test scenarios with various states.
