Files

5.8 KiB

Payment Security Fixes

Overview

Security enhancements have been added to the payment flow in app/Http/Livewire/Pay.php to prevent tampering with front-end properties via browser developer tools or by modifying Livewire component state.

Fixes Applied

1. Balance Limit Check Now Blocks Execution

Location: Pay.php, checkBalanceLimits() method

What was wrong:

  • The method set isPaymentLimitErrorModalOpen = true but returned true
  • doTransfer() didn't check the return value, so execution continued
  • A user could tamper with the check by modifying isPaymentLimitErrorModalOpen in the browser console

What was fixed:

  • checkBalanceLimits() now returns a boolean: true if limits OK, false if exceeded
  • doTransfer() now checks the return value: if (!$limitsOk) { return; }
  • If limits are exceeded, the transaction is NOT saved and a security warning is logged

Test Scenario:

// Before fix: User could bypass by setting in browser console:
Livewire.find('pay-component').isPaymentLimitErrorModalOpen = false;
// And payment would still be saved!

// After fix: Even if user tampersthis property, the backend check prevents saving

2. Amount Validation

Location: Pay.php, doTransfer() method

What was added:

// Security: Validate amount is a valid positive number
if (!is_numeric($amount) || $amount <= 0) {
    $warningMessage = 'Security: Invalid payment amount (must be positive number)';
    return $this->logAndReport(...);
}

Prevents:

  • Negative amounts (e.g., amount = -50)
  • Zero amount (e.g., amount = 0)
  • Non-numeric values (e.g., amount = 'abc')

3. FromAccountId Validation

Location: Pay.php, doTransfer() method (added first)

What was added:

// Security: Verify fromAccountId is an integer (prevent tampering)
if (!is_int($fromAccountId) || $fromAccountId <= 0) {
    $warningMessage = 'Security: Invalid From account ID';
    return $this->logAndReport(...);
}

Prevents:

  • Non-integer account IDs
  • Negative or zero account IDs
  • String values passed as account ID

Note: This validation happens BEFORE the ownership check, catching obvious tampering early.

4. ToAccountId Validation

Location: Pay.php, doTransfer() method

What was added:

// Security: Verify toAccountId is an integer (prevent tampering)
if (!is_int($toAccountId) || $toAccountId <= 0) {
    $warningMessage = 'Security: Invalid To account ID';
    return $this->logAndReport(...);
}

Prevents:

  • Same issues as FromAccountId validation

5. Existing Ownership Check (Still in Place)

Location: Pay.php, doTransfer() method

This was already in place and remains:

$accountsInfo = collect($transactionController->getAccountsInfo());
if (!$accountsInfo->contains('id', $fromAccountId)) {
    $warningMessage = 'Unauthorized payment attempt: illegal access of From account';
    return $this->logAndReport(...);
}

Why this is important:

  • Even though the browser interface only shows accounts the user owns, a user could tamper with Livewire to send a fromAccountId for an account they don't own
  • This backend check verifies the FromAccountId belongs to the active session profile
  • The check happens AFTER type validation but BEFORE the transaction is saved

Security Log Messages

All security violations are logged with a [TRANSFER] prefix. To check for security violations:

grep "\[TRANSFER\].*Security" storage/logs/laravel-*.log

Example log entries:

[2026-06-06 14:30:45] local.WARNING: [TRANSFER] Security: Invalid payment amount (must be positive number)
[2026-06-06 14:31:12] local.WARNING: [TRANSFER] Security: Invalid From account ID
[2026-06-06 14:32:33] local.WARNING: [TRANSFER] Security: Invalid To account ID

Validation Order

The checks happen in this order in doTransfer():

  1. Permission check - Does user have right to make payments?
  2. Amount validation - Is amount numeric and positive?
  3. FromAccountId type check - Is it a positive integer?
  4. ToAccountId type check - Is it a positive integer?
  5. Account existence check - Do both accounts exist in DB?
  6. Account ownership check - Does FromAccountId belong to active profile?
  7. Accountable check - Are both account holders not removed?
  8. Same account check - Are From and To different?
  9. Transaction type check - Is type allowed?
  10. Balance limits check - Does payment exceed limits?

If ANY check fails, execution stops and the transaction is NOT saved.

What's NOT Fixed (By Design)

These are intentionally NOT checked in the backend because they're enforced by the database schema:

  • Transaction immutability - Transactions cannot be modified/deleted (DB user permissions)
  • Account balance accuracy - Balance is calculated from immutable transactions (MySQL window functions)
  • Transaction type validity - Enforced by foreign key constraint

Testing

Manual Testing Instructions:

  1. Make a normal payment to verify it works

  2. Open browser console and try:

    // Try to set negative amount
    Livewire.find('pay-component-id').set('amount', -50);
    // Click Pay button - should be blocked with security log entry
    
  3. Try to inject invalid account IDs:

    Livewire.find('pay-component-id').set('toAccountId', -1);
    // Should be blocked with security log entry
    
  4. Check logs for security violations:

    tail -f storage/logs/laravel-$(date +%Y-%m-%d).log | grep "\[TRANSFER\]"
    
  • PaymentSecurityTest.php - Automated tests for security validations (if needed)
  • app/Http/Controllers/TransactionController.php - API endpoint that also validates payments
  • app/Observers/TransactionObserver.php - Clears caches on successful transaction creation