From ef19221b620138ff236d263d6ab054b2bfaa359a Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Sun, 1 Nov 2026 16:46:03 +0100 Subject: [PATCH] security: add HTTP security headers, rate limiting on sensitive routes, and file upload extension validation --- app/Http/Controllers/CartController.php | 2 +- app/Http/Controllers/ShopController.php | 2 +- app/Http/Middleware/AddSecurityHeaders.php | 27 ++++++++++++++++++++++ bootstrap/app.php | 2 ++ resources/views/shop/show.blade.php | 2 +- routes/web.php | 16 ++++++++----- tests/Feature/AdminContentTest.php | 8 +++++++ 7 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 app/Http/Middleware/AddSecurityHeaders.php diff --git a/app/Http/Controllers/CartController.php b/app/Http/Controllers/CartController.php index 548566a..7845e1f 100644 --- a/app/Http/Controllers/CartController.php +++ b/app/Http/Controllers/CartController.php @@ -94,7 +94,7 @@ class CartController extends Controller $validated = $request->validate([ 'model_name' => 'nullable|string|max:255', 'model_url' => 'nullable|url|max:1000', - 'model_file' => 'nullable|file|max:51200', // 50MB + 'model_file' => 'nullable|file|extensions:stl,3mf,obj,step,stp,zip|max:51200', // 50MB 'size' => 'required|in:klein,medium,groot', 'material' => 'required|string', 'color' => 'required|string', diff --git a/app/Http/Controllers/ShopController.php b/app/Http/Controllers/ShopController.php index 98b1352..ddf7b46 100644 --- a/app/Http/Controllers/ShopController.php +++ b/app/Http/Controllers/ShopController.php @@ -106,7 +106,7 @@ class ShopController extends Controller 'preferred_material' => 'nullable|string|max:50', 'preferred_color' => 'nullable|string|max:50', 'dimensions' => 'nullable|string|max:100', - 'model_file' => 'nullable|file|max:51200', // max 50MB (stl, step, obj, zip) + 'model_file' => 'nullable|file|extensions:stl,3mf,obj,step,stp,zip|max:51200', // max 50MB (stl, step, obj, zip) ]); $filePath = null; diff --git a/app/Http/Middleware/AddSecurityHeaders.php b/app/Http/Middleware/AddSecurityHeaders.php new file mode 100644 index 0000000..e45fd0b --- /dev/null +++ b/app/Http/Middleware/AddSecurityHeaders.php @@ -0,0 +1,27 @@ +headers->set('X-Frame-Options', 'SAMEORIGIN'); + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + + return $response; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 8d87bdb..6bb05bc 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: '*'); $middleware->web(append: [ + AddSecurityHeaders::class, CheckWebshopOffline::class, ]); $middleware->alias([ diff --git a/resources/views/shop/show.blade.php b/resources/views/shop/show.blade.php index 2b4cd89..860e586 100644 --- a/resources/views/shop/show.blade.php +++ b/resources/views/shop/show.blade.php @@ -204,7 +204,7 @@ Over deze 3D print
- {!! nl2br($product->description) !!} + {!! nl2br(strip_tags($product->description, '


diff --git a/routes/web.php b/routes/web.php index 69e34d1..2fbed05 100644 --- a/routes/web.php +++ b/routes/web.php @@ -20,8 +20,12 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; -// Health & Diagnostics +// Health & Diagnostics (Alleen toegankelijk in debug-modus of voor ingelogde admins) Route::get('/health-debug', function () { + if (! config('app.debug') && ! (auth()->check() && auth()->user()->isAdmin())) { + abort(404); + } + $dbPath = config('database.connections.sqlite.database'); $diagnostics = [ @@ -78,23 +82,23 @@ Route::get('/winkelwagen/aantal', [CartController::class, 'count'])->name('cart. // Afrekenen & Bevestiging Route::get('/afrekenen', [CheckoutController::class, 'index'])->name('checkout.index'); -Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store'); +Route::post('/afrekenen', [CheckoutController::class, 'store'])->name('checkout.store')->middleware('throttle:10,1'); Route::get('/bestelling-ontvangen/{orderNumber}', [CheckoutController::class, 'success'])->name('checkout.success'); // Authenticatie & Klantenportaal Route::middleware('guest')->group(function () { Route::get('/inloggen', [LoginController::class, 'showLoginForm'])->name('login'); - Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit'); + Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit')->middleware('throttle:5,1'); Route::redirect('/login', '/inloggen'); Route::get('/registreren', [RegisterController::class, 'showRegistrationForm'])->name('register'); - Route::post('/registreren', [RegisterController::class, 'register'])->name('register.submit'); + Route::post('/registreren', [RegisterController::class, 'register'])->name('register.submit')->middleware('throttle:6,1'); Route::redirect('/register', '/registreren'); Route::get('/wachtwoord-vergeten', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request'); - Route::post('/wachtwoord-vergeten', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email'); + Route::post('/wachtwoord-vergeten', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email')->middleware('throttle:3,1'); Route::get('/wachtwoord-resetten/{token}', [ForgotPasswordController::class, 'showResetForm'])->name('password.reset'); - Route::post('/wachtwoord-resetten', [ForgotPasswordController::class, 'reset'])->name('password.update'); + Route::post('/wachtwoord-resetten', [ForgotPasswordController::class, 'reset'])->name('password.update')->middleware('throttle:5,1'); }); Route::middleware('auth')->group(function () { diff --git a/tests/Feature/AdminContentTest.php b/tests/Feature/AdminContentTest.php index e4d4b0e..dd96ed9 100644 --- a/tests/Feature/AdminContentTest.php +++ b/tests/Feature/AdminContentTest.php @@ -303,4 +303,12 @@ class AdminContentTest extends TestCase $guestOnlineResponse = $this->get(route('home')); $guestOnlineResponse->assertStatus(200); } + + public function test_site_sends_modern_security_headers(): void + { + $response = $this->get('/'); + $response->assertHeader('X-Frame-Options', 'SAMEORIGIN'); + $response->assertHeader('X-Content-Type-Options', 'nosniff'); + $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + } }