You've already forked Atomcms-edit
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:
@@ -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',
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -17,7 +17,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'driver' => 'bcrypt',
|
'driver' => 'argon2id',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
+1
-1
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user