fix: address remaining security audit findings

- Add block-demo middleware to all authenticated route groups
  (settings, web routes, open-banking, api)
- Add rate limiting (throttle:120,1) to API routes
- Add rate limiting (throttle:10,1) to OAuth callback
- Replace trustProxies(at: '*') with env-based TRUSTED_PROXIES config
- Clear state_token on EnableBanking session creation failure
- Move Sentry auth token from hardcoded to SENTRY_AUTH_TOKEN env
- Replace Blade {{ }} with @json() for XSS-safe JS variable injection
- Add limit(500) + has_more flag to TransactionSyncController
- Add since date validation to TransactionSyncController
- Add user_id scoping to Api/TransactionController bulkUpdate
This commit is contained in:
anshulnitin-cyberspace 2026-07-03 17:21:31 +05:30 committed by Víctor Falcón
parent 021cb66643
commit c4af43786b
8 changed files with 29 additions and 12 deletions

View File

@ -112,11 +112,14 @@ class TransactionController extends Controller
abort(403, 'Some transactions do not belong to the authenticated user.');
}
$userId = $request->user()->id;
foreach ($validated['transactions'] as $data) {
$updateData = collect($data)->except('id')->toArray();
Transaction::query()
->where('id', $data['id'])
->where('user_id', $userId)
->toBase()
->update($updateData);
}

View File

@ -210,6 +210,10 @@ class AuthorizationController extends Controller
} catch (\Throwable $e) {
Log::error('EnableBanking session creation failed', ['error' => $e->getMessage()]);
if ($connection) {
$connection->update(['state_token' => null]);
}
return $this->finishRedirect($errorRedirectRoute, $errorRedirectParams, 'error', 'Failed to connect to your bank. Please try again.');
}

View File

@ -15,21 +15,27 @@ class TransactionSyncController extends Controller
*/
public function index(Request $request): JsonResponse
{
$validated = $request->validate([
'since' => ['nullable', 'date'],
]);
$query = Transaction::query()
->where('user_id', $request->user()->id);
if ($request->has('since')) {
$query->where('updated_at', '>', $request->input('since'));
if ($validated['since'] ?? null) {
$query->where('updated_at', '>', $validated['since']);
}
$transactions = $query
->with('labels')
->orderBy('transaction_date', 'desc')
->orderBy('updated_at', 'desc')
->limit(500)
->get();
return response()->json([
'data' => $transactions,
'has_more' => $transactions->count() === 500,
]);
}
}

View File

@ -30,8 +30,10 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->encryptCookies(except: ['appearance', 'sidebar_state', 'chart-color-scheme']);
$trustedProxies = env('TRUSTED_PROXIES');
$middleware->trustProxies(
at: '*',
at: is_string($trustedProxies) ? explode(',', $trustedProxies) : null,
headers: Request::HEADER_X_FORWARDED_FOR
| Request::HEADER_X_FORWARDED_HOST
| Request::HEADER_X_FORWARDED_PORT

View File

@ -10,7 +10,7 @@
{{-- Inline script to detect system dark mode preference and apply it immediately --}}
<script>
(function() {
const appearance = '{{ $appearance ?? "system" }}';
const appearance = @json($appearance ?? 'system');
if (appearance === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
@ -20,7 +20,7 @@
}
}
var chartScheme = '{{ $chartColorScheme ?? "colorful" }}';
var chartScheme = @json($chartColorScheme ?? 'colorful');
try {
chartScheme = localStorage.getItem('chart-color-scheme') || chartScheme;

View File

@ -13,7 +13,7 @@ use App\Http\Controllers\EncryptionController;
use App\Http\Controllers\Sync\TransactionSyncController;
use Illuminate\Support\Facades\Route;
Route::middleware(['web', 'auth'])->group(function () {
Route::middleware(['web', 'auth', 'block-demo', 'throttle:120,1'])->group(function () {
// Encryption (legacy decrypt-migration support only)
Route::get('encryption/message', [EncryptionController::class, 'getMessage']);

View File

@ -20,7 +20,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware('auth')->group(function () {
Route::middleware(['auth', 'block-demo'])->group(function () {
Route::redirect('settings', '/settings/accounts');
Route::get('settings/account', [ProfileController::class, 'account'])->name('account.edit');

View File

@ -99,7 +99,7 @@ Route::get('terms', function () {
return Inertia::render('terms');
})->name('terms');
Route::middleware(['auth', 'verified'])->group(function () {
Route::middleware(['auth', 'verified', 'block-demo'])->group(function () {
Route::get('subscribe', [SubscriptionController::class, 'index'])->name('subscribe');
Route::get('subscribe/checkout', [SubscriptionController::class, 'checkout'])->name('subscribe.checkout');
Route::get('subscribe/success', [SubscriptionController::class, 'success'])->name('subscribe.success');
@ -135,7 +135,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::delete('integration-requests/{integrationRequest}/vote', [IntegrationRequestController::class, 'removeVote'])->name('integration-requests.vote.destroy');
});
Route::middleware(['auth', 'verified', 'onboarded', 'subscribed'])->group(function () {
Route::middleware(['auth', 'verified', 'onboarded', 'subscribed', 'block-demo'])->group(function () {
Route::get('dashboard', DashboardController::class)->name('dashboard');
// Renders the dashboard with the integration-requests drawer opened on top.
Route::get('integration-requests', [IntegrationRequestController::class, 'index'])->name('integration-requests.index');
@ -159,11 +159,13 @@ Route::middleware(['auth', 'verified', 'onboarded', 'subscribed'])->group(functi
// The bank authorization callback is intentionally unauthenticated: iOS PWAs hand the
// redirect back to Safari where the app session does not exist. The connection is
// resolved from the signed state token EnableBanking echoes back instead.
Route::get('open-banking/callback', [AuthorizationController::class, 'callback'])->name('open-banking.callback');
Route::get('open-banking/callback', [AuthorizationController::class, 'callback'])
->middleware('throttle:10,1')
->name('open-banking.callback');
// Open-banking routes are accessible without the onboarded/subscribed middleware
// so that users can connect their bank during the onboarding flow.
Route::middleware(['auth', 'verified'])->prefix('open-banking')->group(function () {
Route::middleware(['auth', 'verified', 'block-demo'])->prefix('open-banking')->group(function () {
Route::get('institutions', [InstitutionController::class, 'index'])->name('open-banking.institutions');
Route::post('authorize', [AuthorizationController::class, 'store'])->name('open-banking.authorize');
Route::post('connections/{connection}/reauthorize', [AuthorizationController::class, 'reauthorize'])->name('open-banking.reauthorize');
@ -184,7 +186,7 @@ Route::middleware(['auth', 'verified'])->prefix('open-banking')->group(function
->name('open-banking.interactive-brokers.connect');
});
Route::middleware(['auth', 'verified', 'onboarded', 'subscribed'])->group(function () {
Route::middleware(['auth', 'verified', 'onboarded', 'subscribed', 'block-demo'])->group(function () {
Route::get('budgets', [BudgetController::class, 'index'])->name('budgets.index');
Route::post('budgets', [BudgetController::class, 'store'])->name('budgets.store');
Route::get('budgets/{budget}', [BudgetController::class, 'show'])->name('budgets.show');