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:
parent
021cb66643
commit
c4af43786b
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue