fix(security): patch critical security vulnerabilities

- Remove User::$guarded = [] to prevent mass assignment attacks
- Enable SQL strict mode and disable emulated prepares (SQL injection prevention)
- Switch password hashing from bcrypt to argon2id (stronger algorithm)
- Enable session encryption to protect session data at rest
- Restrict TrustProxies to localhost only (prevent IP spoofing)
- Restrict CORS allowed_methods via env variable instead of wildcard
- Add PayPal amount mismatch detection to prevent payment manipulation
- Add double-capture prevention (idempotency check)
- Add expected_amount column to transactions table for verification
This commit is contained in:
root
2026-05-19 19:37:15 +02:00
parent 05fc7b04bc
commit 7f59024bef
8 changed files with 56 additions and 14 deletions
+29 -6
View File
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AccountTopupFormRequest; use App\Http\Requests\AccountTopupFormRequest;
use App\Models\Shop\WebsitePaypalTransaction;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -68,6 +69,7 @@ class PayPalController extends Controller
$request->user()->transactions()->create([ $request->user()->transactions()->create([
'transaction_id' => $response['id'], 'transaction_id' => $response['id'],
'amount' => 0, 'amount' => 0,
'expected_amount' => $amount,
]); ]);
return redirect()->away($links['href']); return redirect()->away($links['href']);
@@ -79,7 +81,7 @@ class PayPalController extends Controller
); );
} }
public function successful(Request $request): Response public function successful(Request $request): Response|RedirectResponse
{ {
$request->validate([ $request->validate([
'token' => 'required', 'token' => 'required',
@@ -92,8 +94,12 @@ class PayPalController extends Controller
return to_route('shop.index')->withErrors(['message' => __('Something went wrong, please try again later')]); return to_route('shop.index')->withErrors(['message' => __('Something went wrong, please try again later')]);
} }
if ($transaction->status === self::STATUS_COMPLETED) {
return to_route('shop.index')->withErrors(['message' => __('Transaction already processed')]);
}
$response = $this->getProvider()->capturePaymentOrder($request['token']); $response = $this->getProvider()->capturePaymentOrder($request['token']);
$paymentDetails = $response['purchase_units'][0]['payments']['captures'][0]; $paymentDetails = $response['purchase_units'][0]['payments']['captures'][0] ?? null;
if (! isset($response['status'], $paymentDetails)) { if (! isset($response['status'], $paymentDetails)) {
Log::error('Invalid response from PayPal', ['response' => $response]); Log::error('Invalid response from PayPal', ['response' => $response]);
@@ -112,11 +118,28 @@ class PayPalController extends Controller
return to_route('shop.index')->withErrors(['message' => __('Something went wrong, please check your paypal account to make sure nothing was deducted and try again')]); return to_route('shop.index')->withErrors(['message' => __('Something went wrong, please check your paypal account to make sure nothing was deducted and try again')]);
} }
$paymentDetails = $response['purchase_units'][0]['payments']['captures'][0]; $capturedAmount = (float) $paymentDetails['amount']['value'];
$expectedAmount = (float) ($transaction->expected_amount ?? 0);
if ($expectedAmount > 0 && abs($capturedAmount - $expectedAmount) > 0.01) {
Log::warning('PayPal amount mismatch', [
'transaction_id' => $transaction->transaction_id,
'expected' => $expectedAmount,
'captured' => $capturedAmount,
]);
$transaction->update([
'status' => 'AMOUNT_MISMATCH',
'description' => 'Captured amount does not match expected amount',
'amount' => $capturedAmount,
]);
return to_route('shop.index')->withErrors(['message' => __('Transaction amount mismatch detected')]);
}
$transaction->update([ $transaction->update([
'status' => $paymentDetails['status'], 'status' => $paymentDetails['status'],
'amount' => $paymentDetails['amount']['value'], 'amount' => $capturedAmount,
'currency' => $paymentDetails['amount']['currency_code'], 'currency' => $paymentDetails['amount']['currency_code'],
]); ]);
@@ -126,12 +149,12 @@ class PayPalController extends Controller
); );
} }
$user->increment('website_balance', $paymentDetails['amount']['value']); $user->increment('website_balance', $capturedAmount);
return to_route('shop.index')->with('success', __('Transaction successful')); return to_route('shop.index')->with('success', __('Transaction successful'));
} }
public function cancelled(Request $request): Response public function cancelled(Request $request): RedirectResponse
{ {
$request->validate([ $request->validate([
'token' => 'required', 'token' => 'required',
+1 -1
View File
@@ -15,7 +15,7 @@ class TrustProxies extends Middleware
* @var array<int, string>|string|null * @var array<int, string>|string|null
*/ */
#[\Override] #[\Override]
protected $proxies; protected $proxies = '127.0.0.1,::1';
/** /**
* The headers that should be used to detect proxies. * The headers that should be used to detect proxies.
-2
View File
@@ -127,8 +127,6 @@ class User extends Authenticatable implements FilamentUser, HasName
#[\Override] #[\Override]
protected $fillable = ['username', 'mail', 'password', 'account_created', 'last_login', 'motto', 'look', 'credits', 'auth_ticket', 'home_room', 'ip_register', 'ip_current', 'referral_code', 'preferences', 'team_id', 'avatar_background', 'home_background', 'pincode', 'secret_key', 'extra_rank', 'is_hidden', 'background_id', 'background_stand_id', 'background_overlay_id', 'radio_points', 'pixels', 'points', 'online', 'gender', 'rank', 'mail_verified', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at']; protected $fillable = ['username', 'mail', 'password', 'account_created', 'last_login', 'motto', 'look', 'credits', 'auth_ticket', 'home_room', 'ip_register', 'ip_current', 'referral_code', 'preferences', 'team_id', 'avatar_background', 'home_background', 'pincode', 'secret_key', 'extra_rank', 'is_hidden', 'background_id', 'background_stand_id', 'background_overlay_id', 'radio_points', 'pixels', 'points', 'online', 'gender', 'rank', 'mail_verified', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'];
protected $guarded = [];
#[\Override] #[\Override]
protected $hidden = ['id', 'password', 'remember_token']; protected $hidden = ['id', 'password', 'remember_token'];
+1 -1
View File
@@ -19,7 +19,7 @@ return [
'paths' => ['api/*', 'sanctum/csrf-cookie'], 'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'], 'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''),
'allowed_origins' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_ORIGINS', ''))), fn ($v) => $v !== ''), 'allowed_origins' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_ORIGINS', ''))), fn ($v) => $v !== ''),
+1 -2
View File
@@ -53,13 +53,12 @@ return [
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '', 'prefix' => '',
'prefix_indexes' => true, 'prefix_indexes' => true,
'strict' => false, 'strict' => true,
'engine' => 'InnoDB', 'engine' => 'InnoDB',
'sticky' => true, 'sticky' => true,
'options' => extension_loaded('pdo_mysql') ? array_filter([ 'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
Mysql::ATTR_INIT_COMMAND => 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,"ONLY_FULL_GROUP_BY",""))', Mysql::ATTR_INIT_COMMAND => 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,"ONLY_FULL_GROUP_BY",""))',
Mysql::ATTR_EMULATE_PREPARES => true,
]) : [], ]) : [],
], ],
+1 -1
View File
@@ -17,7 +17,7 @@ return [
| |
*/ */
'driver' => 'bcrypt', 'driver' => 'argon2id',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
+1 -1
View File
@@ -46,7 +46,7 @@ return [
| |
*/ */
'encrypt' => false, 'encrypt' => env('SESSION_ENCRYPT', true),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('website_paypal_transactions', function (Blueprint $table) {
$table->decimal('expected_amount', 10, 2)->nullable()->after('amount');
});
}
public function down(): void
{
Schema::table('website_paypal_transactions', function (Blueprint $table) {
$table->dropColumn('expected_amount');
});
}
};