Files
alrahma_sunday_school_api/docs/security/security_patch.diff
T

363 lines
16 KiB
Diff

--- .env
--- school_api_orig/school_api/.env 2026-06-04 07:50:20.000000000 +0000
+++ school_api_work/school_api/.env 2026-06-04 17:35:04.127545553 +0000
@@ -1,9 +1,9 @@
APP_NAME=Alrahma_API
-APP_ENV=local
-APP_KEY=base64:RfIZKqgXC9seghl2Jqs2MgLh1X1Z7APRsnhUK8CgWx8=
-APP_DEBUG=true
+APP_ENV=production
+APP_KEY=base64:CHANGE_ME_GENERATE_WITH_php_artisan_key_generate
+APP_DEBUG=false
APP_TIMEZONE=America/New_York
-APP_URL=http://localhost:8080
+APP_URL=https://example.com
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
@@ -22,18 +22,21 @@
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=school_api
-DB_USERNAME=root
-DB_PASSWORD=root
-APP_URL=http://127.0.0.1:8000
+DB_USERNAME=school_api_user
+DB_PASSWORD=CHANGE_ME_DB_PASSWORD
+APP_URL=https://example.com
# ----------------------------
# SESSION
# ----------------------------
SESSION_DRIVER=file
SESSION_LIFETIME=120
-SESSION_ENCRYPT=false
+SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=null
+SESSION_SECURE_COOKIE=true
+SESSION_HTTP_ONLY=true
+SESSION_SAME_SITE=strict
# ----------------------------
# CACHE & QUEUE
@@ -56,10 +59,10 @@
MAIL_PROFILE_DEFAULT=default
MAIL_DEFAULT_HOST=smtp.gmail.com
MAIL_DEFAULT_PORT=587
-MAIL_DEFAULT_USER=alrahma.sunday.school@gmail.com
-MAIL_DEFAULT_PASS="psnp emdq dykw ypul"
+MAIL_DEFAULT_USER=CHANGE_ME_SMTP_USER
+MAIL_DEFAULT_PASS=CHANGE_ME_SMTP_PASSWORD
MAIL_DEFAULT_ENCRYPTION=tls
-MAIL_DEFAULT_FROM_EMAIL="alrahma.sunday.school@gmail.com"
+MAIL_DEFAULT_FROM_EMAIL="no-reply@example.com"
MAIL_DEFAULT_FROM_NAME="Alrahma API"
MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com"
MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School"
@@ -102,7 +105,7 @@
# ----------------------------
# JWT
# ----------------------------
-JWT_SECRET=Uj8rGnYcXMgeRc5qCIn9Wn03tYo1pCsBz1Biou8T9zWtaNxBYi3P4NP5vuFiXHmd
+JWT_SECRET=CHANGE_ME_GENERATE_WITH_php_artisan_jwt_secret
JWT_ALGO=HS256
JWT_TTL=60
JWT_REFRESH_TTL=20160
--- routes/web.php
--- school_api_orig/school_api/routes/web.php 2026-05-29 00:28:37.000000000 +0000
+++ school_api_work/school_api/routes/web.php 2026-06-04 17:35:04.129001398 +0000
@@ -61,7 +61,7 @@
Route::middleware('web')->group(function () {
Route::get('login', [AuthSessionController::class, 'loginMask'])->name('login');
Route::post('user/login', [AuthSessionController::class, 'login'])->name('auth.session.login');
- Route::get('logout', [AuthSessionController::class, 'logout'])->name('auth.session.logout');
+ Route::post('logout', [AuthSessionController::class, 'logout'])->name('auth.session.logout');
Route::get('select-role', [AuthSessionController::class, 'selectRole'])->name('auth.session.select-role');
Route::post('set-role', [AuthSessionController::class, 'setRole'])->name('auth.session.set-role');
--- routes/api.php
--- school_api_orig/school_api/routes/api.php 2026-06-04 17:05:37.000000000 +0000
+++ school_api_work/school_api/routes/api.php 2026-06-04 17:35:04.130570862 +0000
@@ -126,8 +126,8 @@
use App\Http\Controllers\Api\System\AccessDeniedController;
use App\Http\Controllers\Api\Certificates\CertificateController;
// Public auth aliases without the `/v1` prefix.
-Route::post('login', [AuthController::class, 'login']);
-Route::post('register', [RegisterController::class, 'store']);
+Route::post('login', [AuthController::class, 'login'])->middleware('throttle:10,1');
+Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:5,1');
/*
| LanguageTool proxy for legacy and current clients.
@@ -160,12 +160,15 @@
});
Route::get('confirm_authorized_user', [AuthorizedUserInviteController::class, 'confirm'])
+ ->middleware('throttle:20,1')
->name('api.invite.confirm');
Route::get('set_authorized_user_password/{authorizedUserId}', [AuthorizedUserInviteController::class, 'setPasswordForm'])
->whereNumber('authorizedUserId')
+ ->middleware('throttle:20,1')
->name('api.invite.set-password-form');
Route::post('set_authorized_user_password/{authorizedUserId}', [AuthorizedUserInviteController::class, 'savePassword'])
->whereNumber('authorizedUserId')
+ ->middleware('throttle:10,1')
->name('api.invite.set-password');
/*
@@ -184,13 +187,13 @@
Route::prefix('v1')->group(function () {
// Public auth aliases without the `/auth` prefix.
- Route::post('login', [AuthController::class, 'login']);
- Route::post('register', [RegisterController::class, 'store']);
+ Route::post('login', [AuthController::class, 'login'])->middleware('throttle:10,1');
+ Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:5,1');
Route::prefix('auth')->group(function () {
- Route::get('register/captcha', [RegisterController::class, 'captcha']);
- Route::post('register', [RegisterController::class, 'store']);
- Route::post('login', [AuthController::class, 'login']);
+ Route::get('register/captcha', [RegisterController::class, 'captcha'])->middleware('throttle:30,1');
+ Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:5,1');
+ Route::post('login', [AuthController::class, 'login'])->middleware('throttle:10,1');
Route::post('refresh', [AuthController::class, 'refresh'])->middleware('jwt.auth');
Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:api');
Route::get('me', [AuthController::class, 'me'])->middleware('auth:api');
@@ -208,7 +211,7 @@
Route::post('contact', [PageController::class, 'submitContact']);
});
- Route::post('contact', [SupportContactController::class, 'send']);
+ Route::post('contact', [SupportContactController::class, 'send'])->middleware('throttle:5,1');
/*
| Badge scan kiosk (public, throttled). Logs: authenticated staff.
--- bootstrap/app.php
--- school_api_orig/school_api/bootstrap/app.php 2026-05-29 03:38:30.000000000 +0000
+++ school_api_work/school_api/bootstrap/app.php 2026-06-04 17:35:04.128252803 +0000
@@ -15,6 +15,8 @@
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
+ $middleware->append(\App\Http\Middleware\SecurityHeaders::class);
+
$middleware->alias([
// JWT auth
'jwt.auth' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class,
--- app/Http/Middleware/SecurityHeaders.php
--- app/Services/Files/FileServeService.php
--- school_api_orig/school_api/app/Services/Files/FileServeService.php 2026-05-29 00:28:37.000000000 +0000
+++ school_api_work/school_api/app/Services/Files/FileServeService.php 2026-06-04 17:35:04.131136361 +0000
@@ -39,13 +39,10 @@
'Content-Length' => (string) $meta['size'],
'ETag' => $meta['etag'],
'Last-Modified' => $meta['last_modified'],
- 'Cache-Control' => 'public, max-age=86400',
+ 'Cache-Control' => 'private, max-age=300',
+ 'X-Content-Type-Options' => 'nosniff',
];
- if ($nosniff) {
- $headers['X-Content-Type-Options'] = 'nosniff';
- }
-
return response(file_get_contents($meta['path']), 200, $headers);
}
--- app/Services/Events/EventManagementService.php
--- school_api_orig/school_api/app/Services/Events/EventManagementService.php 2026-05-29 00:28:37.000000000 +0000
+++ school_api_work/school_api/app/Services/Events/EventManagementService.php 2026-06-04 17:35:04.131625457 +0000
@@ -103,12 +103,28 @@
return null;
}
+ $allowed = [
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/webp' => 'webp',
+ 'application/pdf' => 'pdf',
+ ];
+
+ $mime = strtolower((string) $file->getMimeType());
+ if (! isset($allowed[$mime])) {
+ throw new \InvalidArgumentException('Unsupported flyer file type.');
+ }
+
+ if ((int) $file->getSize() > 5 * 1024 * 1024) {
+ throw new \InvalidArgumentException('Flyer file too large. Max 5MB.');
+ }
+
$dir = public_path('uploads/event_flyers');
if (!File::exists($dir)) {
File::makeDirectory($dir, 0755, true);
}
- $name = $file->hashName();
+ $name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$file->move($dir, $name);
return 'event_flyers/' . $name;
--- app/Services/Expenses/ExpenseReceiptService.php
--- school_api_orig/school_api/app/Services/Expenses/ExpenseReceiptService.php 2026-05-29 00:28:37.000000000 +0000
+++ school_api_work/school_api/app/Services/Expenses/ExpenseReceiptService.php 2026-06-04 17:35:04.132035612 +0000
@@ -14,8 +14,30 @@
public function storeReceipt(UploadedFile $file): string
{
- $stored = $file->store('receipts');
- return basename($stored);
+ if (! $file->isValid()) {
+ throw new \InvalidArgumentException('Invalid receipt upload.');
+ }
+
+ $allowed = [
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/webp' => 'webp',
+ 'application/pdf' => 'pdf',
+ ];
+
+ $mime = strtolower((string) $file->getMimeType());
+ if (! isset($allowed[$mime])) {
+ throw new \InvalidArgumentException('Unsupported receipt file type.');
+ }
+
+ if ((int) $file->getSize() > 5 * 1024 * 1024) {
+ throw new \InvalidArgumentException('Receipt file too large. Max 5MB.');
+ }
+
+ $filename = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
+ $file->storeAs('receipts', $filename);
+
+ return $filename;
}
public function receiptUrl(?string $filename): ?string
--- app/Services/BroadcastEmail/BroadcastEmailImageService.php
--- school_api_orig/school_api/app/Services/BroadcastEmail/BroadcastEmailImageService.php 2026-05-29 00:28:37.000000000 +0000
+++ school_api_work/school_api/app/Services/BroadcastEmail/BroadcastEmailImageService.php 2026-06-04 17:35:04.132596226 +0000
@@ -17,6 +17,14 @@
public function store(UploadedFile $file): array
{
+ if (! $file->isValid()) {
+ return [
+ 'ok' => false,
+ 'status' => 400,
+ 'error' => 'Invalid upload',
+ ];
+ }
+
$mime = strtolower((string) $file->getMimeType());
if (!isset($this->allowedTypes[$mime])) {
return [
@@ -40,7 +48,7 @@
}
$ext = $this->allowedTypes[$mime];
- $newName = uniqid('em_', true) . '.' . $ext;
+ $newName = 'em_' . bin2hex(random_bytes(16)) . '.' . $ext;
$file->move($targetDir, $newName);
return [
--- app/Services/PrintRequests/PrintRequestsPortalService.php
--- school_api_orig/school_api/app/Services/PrintRequests/PrintRequestsPortalService.php 2026-06-04 06:36:56.000000000 +0000
+++ school_api_work/school_api/app/Services/PrintRequests/PrintRequestsPortalService.php 2026-06-04 17:35:04.133321294 +0000
@@ -387,7 +387,7 @@
public function storeTeacherUpload(array $data, UploadedFile $file, int $teacherId): PrintRequest
{
$this->ensureStorageDirectoryExists();
- $ext = $file->getClientOriginalExtension();
+ $ext = $this->safeExtensionForUpload($file);
$stored = Str::uuid()->toString().($ext !== '' ? '.'.$ext : '');
$file->move($this->storageDirectory(), $stored);
@@ -432,7 +432,7 @@
}
}
}
- $ext = $newFile->getClientOriginalExtension();
+ $ext = $this->safeExtensionForUpload($newFile);
$stored = Str::uuid()->toString().($ext !== '' ? '.'.$ext : '');
$newFile->move($this->storageDirectory(), $stored);
$update['file_path'] = $stored;
@@ -445,6 +445,29 @@
return $request->fresh();
}
+ private function safeExtensionForUpload(UploadedFile $file): string
+ {
+ $allowed = [
+ 'application/pdf' => 'pdf',
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'text/plain' => 'txt',
+ 'application/msword' => 'doc',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
+ ];
+
+ $mime = strtolower((string) $file->getMimeType());
+ if (! isset($allowed[$mime])) {
+ throw new \InvalidArgumentException('Unsupported upload type.');
+ }
+
+ if ((int) $file->getSize() > 5 * 1024 * 1024) {
+ throw new \InvalidArgumentException('Uploaded file too large. Max 5MB.');
+ }
+
+ return $allowed[$mime];
+ }
+
public function applyAdminStatus(PrintRequest $request, string $newStatus, int $adminUserId): PrintRequest
{
$current = (string) $request->status;
--- app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php
--- school_api_orig/school_api/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php 2026-05-29 00:28:37.000000000 +0000
+++ school_api_work/school_api/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php 2026-06-04 17:35:04.133827253 +0000
@@ -47,7 +47,7 @@
}
$validator = Validator::make($request->all(), [
- 'file' => ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt'],
+ 'file' => ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt', 'mimetypes:application/pdf,image/jpeg,image/png,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'page_selection' => ['nullable', 'string', 'regex:/^\s*\d+(?:\s*-\s*\d+)?(?:\s*,\s*\d+(?:\s*-\s*\d+)?)*\s*$/'],
'num_copies' => ['required', 'integer', 'min:1'],
'required_by' => ['required', 'date'],
@@ -97,7 +97,7 @@
];
if ($request->hasFile('file') && $request->file('file')->isValid()) {
- $rules['file'] = ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt'];
+ $rules['file'] = ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt', 'mimetypes:application/pdf,image/jpeg,image/png,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
}
$validator = Validator::make($request->all(), $rules);
--- app/Models/User.php
--- test.sql
--- school_api_orig/school_api/test.sql 2026-05-29 00:27:51.000000000 +0000
+++ school_api_work/school_api/test.sql 2026-06-04 17:35:52.923658533 +0000
@@ -479,15 +479,15 @@
(29, 'total_semester2_days', '13'),
(30, 'delete_email_notify', 'support@alrahmaisgl.org, melabidi@alrahmaisgl.org'),
(31, 'fall_semester_start', '2025-09-21'),
-(32, 'sandbox_secret_paypal', 'Ar0Br3ZhBukQwwdSCxIcSveiu70-Ekt-qqF0nSyYNr3eRAYQKcWTTlhCAKncZiYXmBoPeo4IceV6FjT'),
+(32, 'sandbox_secret_paypal', 'CHANGE_ME_SANDBOX_SECRET_PAYPAL'),
(33, 'sandbox_verifylink_paypal', 'https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature'),
(34, 'sandbox_tokenurl_paypal', 'https://api-m.sandbox.paypal.com/v1/oauth2/token'),
-(35, 'sandbox_clientid_paypal', 'AYsiOekZUvrjgx9C5c554ZeSQ4W6yd0ZX-OHE93_D0fWoa4YXrMmroEeLiAjjdCKkELH8EVZR_yGMLPS'),
-(36, 'sandbox_webhookid_paypal', '0EU27043LC060650D'),
+(35, 'sandbox_clientid_paypal', 'CHANGE_ME_SANDBOX_CLIENTID_PAYPAL'),
+(36, 'sandbox_webhookid_paypal', 'CHANGE_ME_SANDBOX_WEBHOOKID_PAYPAL'),
(37, 'production_verifylink_paypal', 'https://api-m.paypal.com/v1/notifications/verify-webhook-signature'),
-(38, 'production_webhookid_paypal', '0H5876426L0092829'),
-(39, 'production_secret_paypal', 'KKvmE-0cvwx8rEHnEcFaAvn1Rlc3cy07IGIjqGGPR8EqxABbmPodM_JrjRQq2SF2903A4q0DBjjUWai'),
-(40, 'production_clientid_paypal', 'AQhCgmypaitgLgretMqNrh_7mEG5fAmwCECxCxevuLDQfsQVwECFyQOU50byzPzFrwd0cVVD8r2lujIB'),
+(38, 'production_webhookid_paypal', 'CHANGE_ME_PRODUCTION_WEBHOOKID_PAYPAL'),
+(39, 'production_secret_paypal', 'CHANGE_ME_PRODUCTION_SECRET_PAYPAL'),
+(40, 'production_clientid_paypal', 'CHANGE_ME_PRODUCTION_CLIENTID_PAYPAL'),
(41, 'production_tokenurl_paypal', 'https://api-m.paypal.com/v1/oauth2/token'),
(42, 'comment_reviewer', 'administrator, principal'),
(43, 'paypal_mode', 'sandbox'),