# SECURITY_CHECK.md — ISIB / Al Hulool Security Assessment

| Field | Value |
|-------|--------|
| **Application** | ISIB (Integrated Solution Insurance Broker) |
| **Stack** | Laravel 13.x, Livewire 3, Jetstream 5, Fortify, Sanctum |
| **Review type** | Code-only defensive security review (no live DB, no destructive tests) |
| **Standards** | OWASP Top 10 (2021), OWASP API Security Top 10 |
| **Date** | 2026-07-28 |
| **Branch reviewed** | `upgrade/laravel-13` |

---

## Executive summary

A full static review of authentication, authorization, payment/SOAP flows, file handling, sessions, configuration, and dependencies found **multiple confirmed Critical and High issues**. The most severe patterns are: **public self-registration granting full admin access**, **unauthenticated IDOR** on customer documents and follow-up PII, **SQL injection** via concatenated session values, **hardcoded third-party credentials on a public route**, **plaintext password storage** in admin user creation, and **payment business-logic flaws** (offer IDOR + browser-session payment confirmation without amount binding).

Secrets for Thawani/Dhofar/ROP were moved to `.env` / `config/services.php` during the Laravel upgrade (good). CSRF exception list is empty (good). Login rate limiting via Fortify is present (good). API surface is minimal (`GET /api/user` only).

**This report does not modify application code.** Findings marked *Needs Manual Verification* were not proven at runtime.

---

## Overall risk rating

# **Critical**

Deploying or leaving the current codebase publicly reachable without remediating Critical items exposes customer PII, uploaded documents, admin takeover, financial underpayment, and third-party API credentials.

---

## Scope reviewed

| Area | Artifacts |
|------|-----------|
| Authentication / login | Fortify, Jetstream, `User`, Livewire `CreateUser`, session config |
| Authorization | `routes/web.php`, `routes/api.php`, Policies, Livewire admin |
| APIs | `routes/api.php`, Sanctum config |
| Payments / business logic | `ThawaniPaymentController`, `FrontendController` payment methods |
| File handling | `FileUploadController`, public upload routes |
| Injection / XSS / CSRF / SOAP | Models, Blade, Kernel CSRF, `VehicleInsurance` |
| Config / cookies / CORS | `config/*`, `.htaccess`, `.gitignore` |
| Dependencies | `composer.json` / lock audit posture |

**Out of scope for this pass:** live penetration testing, infrastructure/WAF, production DB permission audits, historical git secret mining beyond current tree.

---

## Finding counts

| Severity | Confirmed | Needs Manual Verification | Total |
|----------|-----------|---------------------------|-------|
| Critical | 7 | 0 | 7 |
| High | 12 | 2 | 14 |
| Medium | 14 | 6 | 20 |
| Low / Informational | 8 | 3 | 11 |
| **Total** | **41** | **11** | **52** |

---

# Critical vulnerabilities

---

### Finding title: Public registration grants unrestricted admin access

* **Severity:** Critical  
* **Confidence:** Confirmed  
* **Affected file:** `config/fortify.php`, `routes/web.php`  
* **Function or line number:** Fortify features ~134–135; admin group ~199–202  
* **Vulnerable code:**

```php
// config/fortify.php
'features' => [
    Features::registration(),
    // ...
],

// routes/web.php
Route::group([
    'prefix'     => 'admin',
    'middleware' => 'auth:sanctum',
], function () { /* full admin Livewire */ });
```

* **Risk explanation:** Any visitor can register. Any authenticated user can access `/admin/*`. There is no role, Gate, or policy check. Maps to OWASP A01 Broken Access Control.  
* **Possible business impact:** Full admin takeover, PII exfiltration, insurer/settings tampering, FAQ XSS planting.  
* **Recommended fix:** Disable public registration in production; require invite-only or admin-created users; add `role=admin` (or Spatie Permission) middleware on `/admin/*`.  
* **Secure replacement code:**

```php
// config/fortify.php — production
'features' => [
    // Features::registration(), // disabled
    Features::updateProfileInformation(),
    Features::updatePasswords(),
],

// routes/web.php
Route::group([
    'prefix' => 'admin',
    'middleware' => ['auth:sanctum', 'can:access-admin'],
], function () { /* ... */ });

// AuthServiceProvider
Gate::define('access-admin', fn ($user) => (bool) $user->is_admin);
```

* **Verification steps:** Register a fresh account; confirm `/admin` returns 403. Create admin via artisan/seeder only; confirm admin can access.

---

### Finding title: IDOR — unauthenticated view of any customer uploaded file by ID

* **Severity:** Critical  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Controllers/FileUploadController.php`  
* **Function or line number:** `viewCustomerDoc` lines 36–61; routes `GET /view-uploaded-file/{id}` (public, duplicated ~108 and ~257)  
* **Vulnerable code:**

```php
public function viewCustomerDoc($id){
    $query = CustomerDocument::find($id);
    if(!isset($query) || empty($query)){
        abort(404);
    }
    $filePath = $query->path;
    // NO check that document belongs to session('request_id')
    $pdfContent = Storage::disk('public')->get($filePath);
    return Response::make($pdfContent, 200, [ /* ... */ ]);
}
```

* **Risk explanation:** Sequential IDs allow enumeration of customer KYC/ID documents without authentication. Delete has a session ownership check; view does not. OWASP A01 / API1 Broken Object Level Authorization.  
* **Possible business impact:** Mass PII/document breach; regulatory exposure.  
* **Recommended fix:** Same ownership check as delete; prefer private disk + signed temporary URLs; rate-limit.  
* **Secure replacement code:**

```php
public function viewCustomerDoc($id)
{
    $doc = CustomerDocument::findOrFail($id);
    if ($doc->request_id !== session('request_id')) {
        abort(404);
    }
    abort_unless(Storage::disk('local')->exists($doc->path), 404);

    return Storage::disk('local')->response($doc->path);
}
```

* **Verification steps:** Upload as session A; from another browser without that session, `GET /view-uploaded-file/{id}` must 404.

---

### Finding title: IDOR — follow-up by numeric request ID exposes PII

* **Severity:** Critical  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Controllers/FrontendController.php`  
* **Function or line number:** `submit_follow` lines 126–150 (+ travel/domestic branches)  
* **Vulnerable code:**

```php
public function submit_follow(Request $request){
    $id = $request->id;
    session(['follow_request_id'=>$id]);
    if($type==1){
        $data = DB::table('vehicle_requests')
            ->where('vehicle_requests.id', '=', $id)
            // joins insureds, selected_offer ...
            ->first();
        return view('frontend.follow',['data'=>$data]);
    }
}
```

* **Risk explanation:** Guessable integer IDs return names, email, phone, license, plate, prices, pay status with no OTP/secret.  
* **Possible business impact:** Customer PII harvesting at scale.  
* **Recommended fix:** Require non-guessable token (email OTP, last-4 phone + OTP, or signed lookup token); never return full PII on ID alone.  
* **Secure replacement code:**

```php
public function submit_follow(Request $request)
{
    $data = $request->validate([
        'type' => 'required|in:1,2,3',
        'id' => 'required|integer',
        'otp' => 'required|string',
    ]);
    // Verify OTP bound to insured email/phone for this request id
    if (! FollowUpOtp::verify($data['id'], $data['otp'])) {
        return back()->withErrors(['otp' => 'Invalid code']);
    }
    // Then load and show limited fields
}
```

* **Verification steps:** Attempt follow-up with another user’s request ID without OTP; expect denial.

---

### Finding title: Follow-up documents emailed to attacker-controlled session email

* **Severity:** Critical  
* **Confidence:** Confirmed (logic); mail delivery Needs Manual Verification  
* **Affected file:** `app/Http/Controllers/FrontendController.php`  
* **Function or line number:** `getFollowUpDocumet` (~201–234)  
* **Vulnerable code:**

```php
$request_id = session('follow_request_id');
// loads victim completed_document_path from DB by session id
$email = session('request_email'); // NOT insured email from looked-up row
$this->send_email($email, $request_id, $type, true);
```

* **Risk explanation:** After IDOR on follow-up, attacker can chain document retrieval and receive policy PDF at an email previously stored in their session.  
* **Possible business impact:** Policy document theft.  
* **Recommended fix:** Always email the insured address from DB; require strong auth before document access; use signed URLs with short TTL.  
* **Secure replacement code:**

```php
$insuredEmail = $data->email; // from joined insureds row
abort_unless(filter_var($insuredEmail, FILTER_VALIDATE_EMAIL), 422);
$this->send_email($insuredEmail, $request_id, $type, true);
```

* **Verification steps:** Complete follow-up IDOR then request documents; confirm mail only goes to insured DB email (or requires OTP).

---

### Finding title: SQL injection via concatenated `car_value` in whereRaw

* **Severity:** Critical  
* **Confidence:** Confirmed  
* **Affected file:** `app/Models/VeOffer.php`  
* **Function or line number:** `OfferDetails` lines 54–56  
* **Vulnerable code:**

```php
public function OfferDetails(){
    $val= session('car_value');
  return $this->hasMany(VehicleOfferDetail::class,'vehicle_offer_id','id')
      ->whereRaw("'$val' BETWEEN min_value AND max_value");
}
```

* **Risk explanation:** `car_value` originates from user-controlled Livewire input. Classic SQLi (OWASP A03 Injection).  
* **Possible business impact:** Data theft, data destruction, possible RCE depending on DB/OS.  
* **Recommended fix:** Use parameter binding or query builder comparisons.  
* **Secure replacement code:**

```php
public function OfferDetails()
{
    $val = (float) session('car_value');

    return $this->hasMany(VehicleOfferDetail::class, 'vehicle_offer_id', 'id')
        ->where('min_value', '<=', $val)
        ->where('max_value', '>=', $val);
}
```

* **Verification steps:** Set `car_value` to `1' OR '1'='1` and confirm query fails safely / returns empty, not all rows or error with injected SQL.

---

### Finding title: Hardcoded third-party Basic Auth on public route `/test_`

* **Severity:** Critical  
* **Confidence:** Confirmed  
* **Affected file:** `routes/web.php`  
* **Function or line number:** lines 8–57  
* **Vulnerable code:**

```php
Route::get('test_', function(){
  // ...
  CURLOPT_HTTPHEADER => array(
    'Authorization: Basic aW50…MTIz', // REDACTED — rotate immediately
    // ...
  ),
});
echo $response;
```

* **Risk explanation:** Anyone can hit `/test_` and obtain a live upstream credential plus proxy calls to Anoud API.  
* **Possible business impact:** Third-party account abuse, quota theft, legal/contract breach.  
* **Recommended fix:** Delete route immediately; rotate Basic Auth password at provider; never commit credentials.  
* **Secure replacement code:**

```php
// Remove the route entirely.
// If needed for staging only:
Route::get('test_', function () {
    abort(404);
})->middleware(['auth', 'can:access-admin']);
```

* **Verification steps:** `GET /test_` returns 404; provider confirms credential rotated.

---

### Finding title: Admin CreateUser stores plaintext passwords

* **Severity:** Critical  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Livewire/CreateUser.php`, `app/Models/User.php`  
* **Function or line number:** `store` lines 28–32; `$fillable` includes `password`  
* **Vulnerable code:**

```php
public function store()
{
    $validatedData = $this->validate();
    User::create($validatedData); // password not hashed
}
```

* **Risk explanation:** Fortify registration hashes passwords; admin Livewire path does not. DB compromise or insider read yields usable credentials. Combined with open registration/admin access, impact is extreme.  
* **Possible business impact:** Account takeover of all admin-created users.  
* **Recommended fix:** `Hash::make`; strip `password_confirmation`; add `'password' => 'hashed'` cast; force password resets for existing plaintext users.  
* **Secure replacement code:**

```php
use Illuminate\Support\Facades\Hash;

public function store()
{
    $data = $this->validate();
    unset($data['password_confirmation']);
    $data['password'] = Hash::make($data['password']);
    User::create($data);
}
// User model:
protected function casts(): array
{
    return ['password' => 'hashed'];
}
```

* **Verification steps:** Create user via admin; confirm `password` column is bcrypt/argon hash; login works with chosen password.

---

# High-risk vulnerabilities

---

### Finding title: Payment offer IDOR — client can select any offer price row

* **Severity:** High  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Controllers/FrontendController.php`  
* **Function or line number:** `process_payment` ~510–521; similar for domestic/travel  
* **Vulnerable code:**

```php
$offer_id = $request->offer_id;
$offer = VehicleRequestOffer::find($offer_id);
$price = $offer->price;
```

* **Risk explanation:** Offer is not scoped to `session('vrequest')`. Attacker can charge a cheaper offer from another request. OWASP business logic / financial integrity.  
* **Possible business impact:** Underpayment, revenue loss.  
* **Recommended fix:** Scope query to current request session.  
* **Secure replacement code:**

```php
$offer = VehicleRequestOffer::where('id', $request->offer_id)
    ->where('vehicle_request_id', session('vrequest'))
    ->firstOrFail();
$price = $offer->price;
```

* **Verification steps:** With session for request A, POST/GET payment with offer_id from request B; expect 404.

---

### Finding title: Payment confirmation trusts browser session without amount binding or webhook

* **Severity:** High  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Controllers/ThawaniPaymentController.php`  
* **Function or line number:** `get_session` lines 15–48  
* **Vulnerable code:**

```php
$order_id = session('order_id');
$payment_status = $data->data->payment_status;
if ($payment_status == "paid") {
    DB::table("selected_offer")->where('id', $order_id)
        ->update(['pay_status' => $payment_status, 'transaction_ref' => $client_reference_id]);
}
```

* **Risk explanation:** No verify that Thawani charged amount equals `selected_offer.total_price`; no assert `client_reference_id === order_id`; no webhook; public GET success routes.  
* **Possible business impact:** Paid status inconsistencies, reconciliation fraud, missed payments.  
* **Recommended fix:** Server-side webhook; verify amount + references; idempotent lock.  
* **Secure replacement code:**

```php
abort_unless((string) $client_reference_id === (string) $order_id, 400);
$order = SelectedOffer::lockForUpdate()->findOrFail($order_id);
$paidAmount = (int) data_get($data, 'data.amount'); // per Thawani schema
abort_unless($paidAmount === (int) round($order->total_price * 1000), 400);
abort_unless($order->pay_status !== 'paid', 200); // idempotent
$order->update(['pay_status' => 'paid', 'transaction_ref' => $gatewayPaymentId]);
```

* **Verification steps:** Manual pen-test of success URL with mismatched session/order; confirm rejection.

---

### Finding title: Public test/debug SOAP and email routes

* **Severity:** High  
* **Confidence:** Confirmed  
* **Affected file:** `routes/web.php`, `FrontendController`  
* **Function or line number:** routes ~76–82 (`testemail`, `testapi`, `testapi2`); controller methods ~1322+  
* **Vulnerable code:**

```php
Route::get('testemail', [FrontendController::class, 'test_email']);
Route::get('testapi', [FrontendController::class, 'test_api']);
Route::get('testapi2', [FrontendController::class, 'test_api2']);
```

* **Risk explanation:** Unauthenticated triggers for mail and live SOAP using configured ROP/Dhofar credentials; often dumps responses.  
* **Possible business impact:** Credential abuse, data leakage, DoS on upstream APIs.  
* **Recommended fix:** Remove routes in production; gate behind admin + local env only.  
* **Secure replacement code:**

```php
if (app()->environment('local')) {
    Route::middleware(['auth', 'can:access-admin'])->group(function () {
        // diagnostic routes only
    });
}
```

* **Verification steps:** In production build, routes return 404.

---

### Finding title: SOAP XML injection via unsanitized user fields

* **Severity:** High  
* **Confidence:** Confirmed  
* **Affected file:** `app/Models/VehicleInsurance.php`  
* **Function or line number:** ~120–122 (and ROP bodies ~198–240)  
* **Vulnerable code:**

```php
<civilId>' . $quote_information['license_number'] . '</civilId>
<plateNo>' . $quote_information['plate_number'] . '</plateNo>
```

* **Risk explanation:** Characters like `<` break/alter SOAP structure (XML injection).  
* **Possible business impact:** Quote manipulation, upstream errors, possible logic abuse.  
* **Recommended fix:** `htmlspecialchars($v, ENT_XML1 | ENT_QUOTES, 'UTF-8')` or XML writer.  
* **Secure replacement code:**

```php
$xml = static fn ($v) => htmlspecialchars((string) $v, ENT_XML1 | ENT_QUOTES, 'UTF-8');
$body = '...<civilId>' . $xml($quote_information['license_number']) . '</civilId>...';
```

* **Verification steps:** Submit plate/license containing `</civilId><evil>`; confirm escaped in outbound XML.

---

### Finding title: TLS certificate verification disabled on SOAP/HTTP

* **Severity:** High  
* **Confidence:** Confirmed  
* **Affected file:** `app/Models/VehicleInsurance.php`, `FrontendController.php`  
* **Function or line number:** e.g. VehicleInsurance ~130–132, ~247–258  
* **Vulnerable code:**

```php
Http::accept('application/xml')->timeout(30)->send('post', $wsdl, [
    'body' => $body,
    'verify' => false,
]);
```

* **Risk explanation:** MITM can intercept ROP/Dhofar traffic including credentials and PII.  
* **Possible business impact:** Credential theft, quote tampering.  
* **Recommended fix:** Enable verify; install proper CA bundle; pin if required.  
* **Secure replacement code:**

```php
Http::accept('application/xml')->timeout(30)->withBody($body, 'application/xml')->post($wsdl);
// default verify = true
```

* **Verification steps:** Confirm requests fail against intentionally bad certs; succeed against real endpoints.

---

### Finding title: Stored XSS via FAQ HTML rendering

* **Severity:** High  
* **Confidence:** Confirmed (requires ability to write FAQ — currently any admin/registrant)  
* **Affected file:** `resources/views/frontend/vehicles.blade.php`, `faqs.blade.php`, `travels.blade.php`, etc.  
* **Function or line number:** e.g. vehicles.blade.php ~34 `{!! $faq->answer !!}`; faqs with `html_entity_decode`  
* **Vulnerable code:**

```blade
{!! $faq->answer !!}
```

* **Risk explanation:** Unescaped HTML/JS in public pages. Combined with open admin, trivial stored XSS.  
* **Possible business impact:** Session theft, defacement, malware.  
* **Recommended fix:** Escape by default or sanitize with HTMLPurifier allowlist.  
* **Secure replacement code:**

```blade
{{ $faq->answer }}
{{-- or --}}
{!! clean($faq->answer) !!} {{-- HTMLPurifier --}}
```

* **Verification steps:** Save FAQ answer `<script>alert(1)</script>`; confirm not executed.

---

### Finding title: SESSION_SECURE_COOKIE not forced for HTTPS

* **Severity:** High  
* **Confidence:** Confirmed (misconfiguration risk); runtime Needs Manual Verification on TLS termination  
* **Affected file:** `config/session.php`  
* **Function or line number:** ~171  
* **Vulnerable code:**

```php
'secure' => env('SESSION_SECURE_COOKIE'),
```

* **Risk explanation:** If unset, session cookie may be sent over HTTP → hijacking.  
* **Possible business impact:** Session theft.  
* **Recommended fix:** Set `SESSION_SECURE_COOKIE=true` behind HTTPS.  
* **Secure replacement code:**

```env
SESSION_SECURE_COOKIE=true
```

```php
'secure' => env('SESSION_SECURE_COOKIE', true),
```

* **Verification steps:** Inspect Set-Cookie; Confirm `Secure` flag present on HTTPS responses.

---

### Finding title: Password reset and MFA disabled

* **Severity:** High (missing control / recovery)  
* **Confidence:** Confirmed  
* **Affected file:** `config/fortify.php`  
* **Function or line number:** ~136–142  
* **Vulnerable code:**

```php
// Features::resetPasswords(),
// Features::twoFactorAuthentication([...]),
```

* **Risk explanation:** No self-service recovery; no 2FA for admin accounts holding PII/payments.  
* **Possible business impact:** Permanent lockouts or easier account takeover.  
* **Recommended fix:** Enable reset with throttling; enable 2FA for admins.  
* **Secure replacement code:**

```php
Features::resetPasswords(),
Features::twoFactorAuthentication(['confirm' => true, 'confirmPassword' => true]),
```

* **Verification steps:** Password reset email flow works; admin can enroll 2FA; login requires code.

---

### Finding title: APP_DEBUG defaults to true if env missing

* **Severity:** High  
* **Confidence:** Confirmed (default); current `.env` may set false  
* **Affected file:** `config/app.php`  
* **Function or line number:** line 42  
* **Vulnerable code:**

```php
'debug' => (bool) env('APP_DEBUG', true),
```

* **Risk explanation:** Missing env → stack traces and env leakage (OWASP A05).  
* **Possible business impact:** Full recon for attackers.  
* **Recommended fix:** Default `false`.  
* **Secure replacement code:**

```php
'debug' => (bool) env('APP_DEBUG', false),
```

* **Verification steps:** Unset APP_DEBUG in staging; confirm generic error page.

---

### Finding title: Exception object returned in JSON from payment controller

* **Severity:** High  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Controllers/ThawaniPaymentController.php`  
* **Function or line number:** line 48  
* **Vulnerable code:**

```php
} catch (\Exception $e) {
    return response()->json([$e ], 200);
}
```

* **Risk explanation:** May serialize exception messages/paths to client.  
* **Possible business impact:** Information disclosure aiding further attacks.  
* **Recommended fix:** Log server-side; return generic error.  
* **Secure replacement code:**

```php
} catch (\Throwable $e) {
    report($e);
    return response()->json(['error' => 'Payment verification failed'], 500);
}
```

* **Verification steps:** Force exception; response has no stack/paths.

---

### Finding title: No application authorization policies in use

* **Severity:** High  
* **Confidence:** Confirmed  
* **Affected file:** `app/Providers/AuthServiceProvider.php`, `app/Policies/FaqPolicy.php`  
* **Function or line number:** empty `$policies`; FaqPolicy methods empty; no `authorize()` in Livewire  
* **Vulnerable code:** Policies unused; admin = any auth user.  
* **Risk explanation:** Defense-in-depth absent (A01).  
* **Possible business impact:** Privilege escalation once auth is obtained.  
* **Recommended fix:** Register policies; call `$this->authorize()` in Livewire actions.  
* **Secure replacement code:**

```php
protected $policies = [
    Faq::class => FaqPolicy::class,
];
// In Livewire destroy:
$this->authorize('delete', $faq);
```

* **Verification steps:** Non-admin cannot delete FAQs.

---

### Finding title: Weak password policy (min length only)

* **Severity:** High  
* **Confidence:** Likely (Fortify Password rule defaults)  
* **Affected file:** `app/Actions/Fortify/PasswordValidationRules.php`  
* **Function or line number:** ~16  
* **Vulnerable code:** Uses default Fortify `Password` rule without complexity.  
* **Risk explanation:** Credential stuffing success more likely.  
* **Recommended fix:** Require mixed case, numbers, uncompromised check.  
* **Secure replacement code:**

```php
Password::defaults(fn () => Password::min(12)->mixedCase()->numbers()->uncompromised());
```

* **Verification steps:** Reject `password` / `12345678`.

---

# Medium-risk vulnerabilities

---

### Finding title: CORS allows all origins on API paths

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `config/cors.php`  
* **Function or line number:** lines 18–24  
* **Vulnerable code:**

```php
'allowed_origins' => ['*'],
'allowed_methods' => ['*'],
```

* **Risk explanation:** Broad CORS; mitigated somewhat by `supports_credentials` false. Still risky if tokens used from browsers.  
* **Recommended fix:** Allowlist production fronts only.  
* **Secure replacement code:**

```php
'allowed_origins' => [env('FRONTEND_URL', 'https://isiboman.com')],
```

* **Verification steps:** Cross-origin from evil.com denied.

---

### Finding title: File upload lacks size limit; client filename retained

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Controllers/FileUploadController.php`  
* **Function or line number:** `fileUpload` 66–71  
* **Vulnerable code:**

```php
'file' => 'required|mimes:csv,txt,xlx,xls,pdf,doc,docx,jpg,jpeg,gif,png'
$fileName = time().'_'.$req->file->getClientOriginalName();
storeAs('uploads/customer', $fileName, 'public');
```

* **Risk explanation:** DoS via large files; public disk exposure; filename quirks.  
* **Recommended fix:** `max:5120`; random stored names; private disk.  
* **Secure replacement code:**

```php
'file' => 'required|file|max:5120|mimes:pdf,jpg,jpeg,png',
$path = $req->file('file')->store('uploads/customer', 'local');
```

* **Verification steps:** Reject >5MB; storage path is random UUID.

---

### Finding title: Duplicate public upload routes outside session group

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `routes/web.php`  
* **Function or line number:** ~251–260  
* **Vulnerable code:** Second registration of upload/view/delete without surrounding session middleware group.  
* **Risk explanation:** Increases attack surface / confusion.  
* **Recommended fix:** Keep a single authenticated/session-bound route group.  
* **Secure replacement code:** Delete duplicate route definitions.  
* **Verification steps:** `route:list` shows one of each.

---

### Finding title: Session discount applied at checkout without re-validation

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `FrontendController::process_payment`  
* **Function or line number:** ~ discount via `session('disscount')`  
* **Vulnerable code:** Discount from session subtracted from total without re-checking coupon active/limits.  
* **Risk explanation:** Stale or poisoned session discount underprices policies.  
* **Recommended fix:** Re-load coupon from DB at charge time.  
* **Secure replacement code:**

```php
$coupon = VehicleCoupon::where('code', session('refrence_code'))->where('active', 1)->first();
$discount = $coupon?->percent ?? 0;
```

* **Verification steps:** Deactivate coupon mid-session; checkout must ignore it.

---

### Finding title: Dhofar offer price hardcoded in view

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `resources/views/livewire/vehicles/dhofar_offer.blade.php`  
* **Function or line number:** ~11–13  
* **Vulnerable code:** Hardcoded display/price `500` vs API premium.  
* **Risk explanation:** Integrity mismatch under/over charge.  
* **Recommended fix:** Use API-returned premium only.  
* **Secure replacement code:** Bind blade to `$dhofarPremium` from SOAP response stored server-side.  
* **Verification steps:** Change API premium; UI and charge match.

---

### Finding title: Checkout via GET (CSRF not applicable)

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `routes/web.php`  
* **Function or line number:** vehicles/domestics `process_payment` GET routes  
* **Vulnerable code:** Payment initiation on GET.  
* **Risk explanation:** Drive-by checkout if victim has active quote session.  
* **Recommended fix:** POST + CSRF.  
* **Secure replacement code:**

```php
Route::post('vehicles/process_payment', [FrontendController::class, 'process_payment']);
```

* **Verification steps:** GET returns 405; POST with CSRF works.

---

### Finding title: Short-lived temp_urls token restores session context

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** Livewire `VehiclesForm` / `FrontendController` offers restore  
* **Function or line number:** `Str::random(10)` token; delete-after-use commented  
* **Vulnerable code:** Low-entropy reusable token maps to session JSON.  
* **Risk explanation:** Token guess/leak restores checkout.  
* **Recommended fix:** 32+ bytes random; single-use; short TTL.  
* **Secure replacement code:**

```php
'token' => Str::random(64),
'expires_at' => now()->addMinutes(30),
// delete row on first use
```

* **Verification steps:** Reuse token returns 404.

---

### Finding title: Sanctum API tokens never expire

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `config/sanctum.php`  
* **Function or line number:** `expiration => null`  
* **Vulnerable code:** Tokens do not expire.  
* **Risk explanation:** Stolen token valid indefinitely.  
* **Recommended fix:** Set expiration hours; rotate.  
* **Secure replacement code:**

```php
'expiration' => 60 * 24 * 7, // minutes
```

* **Verification steps:** Token older than TTL rejected.

---

### Finding title: Remember-me vs expire_on_close

* **Severity:** Medium  
* **Confidence:** Needs Manual Verification  
* **Affected file:** `resources/views/auth/login.blade.php`, `config/session.php`  
* **Function or line number:** remember checkbox; `expire_on_close` true  
* **Vulnerable code:** Remember-me can persist beyond browser close.  
* **Risk explanation:** Longer session on shared devices.  
* **Recommended fix:** Disable remember-me for admins; or shorten remember lifetime.  
* **Secure replacement code:** Remove remember checkbox on admin login; or `Auth::login($user, false)`.  
* **Verification steps:** Confirm cookie lifetimes.

---

### Finding title: Mass assignment breadth on Settings / Insureds listing

* **Severity:** Medium  
* **Confidence:** Confirmed (admin-gated today)  
* **Affected file:** `app/Http/Livewire/Settings.php`, `Insureds.php`  
* **Function or line number:** Settings save; `Insured::all()`  
* **Vulnerable code:** Broad updates; full PII list to any auth user.  
* **Risk explanation:** Escalates with broken admin authz.  
* **Recommended fix:** Role checks; paginate/minimize PII fields.  
* **Secure replacement code:** `$this->authorize('update', $setting);`  
* **Verification steps:** Non-admin cannot open insureds.

---

### Finding title: Path-based file_get_contents to admin storage host

* **Severity:** Medium  
* **Confidence:** Needs Manual Verification  
* **Affected file:** `FrontendController` document fetch helpers  
* **Function or line number:** `file_get_contents($this->file_url('https://admin.isiboman.com/storage/'.$filePath))`  
* **Vulnerable code:** Path from DB appended to fixed host.  
* **Risk explanation:** If path attacker-controlled → SSRF/path abuse.  
* **Recommended fix:** Allowlist path prefix; use Storage disk locally.  
* **Secure replacement code:**

```php
abort_unless(str_starts_with($filePath, 'uploads/'), 400);
return Storage::disk('local')->get($filePath);
```

* **Verification steps:** Reject `../` and absolute URLs in path.

---

### Finding title: XXE hardening not explicit on SimpleXML

* **Severity:** Medium  
* **Confidence:** Needs Manual Verification (PHP 8 defaults help)  
* **Affected file:** `VehicleInsurance::xml_to_array`  
* **Function or line number:** ~139–173  
* **Vulnerable code:** `simplexml_load_string` without `LIBXML_NONET`.  
* **Risk explanation:** Hostile XML could expand entities if defaults change.  
* **Recommended fix:** Disable network/entities.  
* **Secure replacement code:**

```php
$prev = libxml_use_internal_errors(true);
$obj = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NONET | LIBXML_NOCDATA);
```

* **Verification steps:** XXE payload does not fetch external DTD.

---

### Finding title: composer audit.block-insecure disabled

* **Severity:** Medium (process)  
* **Confidence:** Confirmed  
* **Affected file:** `composer.json`  
* **Function or line number:** `config.audit.block-insecure`  
* **Vulnerable code:** `"block-insecure": false`  
* **Risk explanation:** Vulnerable packages can be installed without failing CI.  
* **Recommended fix:** Re-enable after upgrade; fail CI on advisories.  
* **Secure replacement code:**

```json
"audit": { "block-insecure": true }
```

* **Verification steps:** `composer audit` in CI fails on known CVE.

---

### Finding title: Upload/auth session binding only (no login)

* **Severity:** Medium  
* **Confidence:** Confirmed  
* **Affected file:** `FileUploadController`  
* **Function or line number:** uses `session('request_id')`  
* **Vulnerable code:** Authorization = possession of PHP session from quote flow.  
* **Risk explanation:** Session fixation/hijack or shared device abuse.  
* **Recommended fix:** Bind uploads to signed request tokens.  
* **Secure replacement code:** Signed middleware validating `request_id` + HMAC.  
* **Verification steps:** Without session, upload/delete fail.

---

# Low-risk / Informational

---

### Finding title: CSRF $except list empty

* **Severity:** Informational  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Middleware/VerifyCsrfToken.php`  
* **Function or line number:** 14–16  
* **Vulnerable code:** Empty `$except` (good).  
* **Risk explanation:** N/A — positive finding.  
* **Recommended fix:** Keep empty.  
* **Secure replacement code:** N/A  
* **Verification steps:** POST without token → 419.

---

### Finding title: Login rate limiting present

* **Severity:** Informational  
* **Confidence:** Confirmed  
* **Affected file:** `app/Providers/FortifyServiceProvider.php`  
* **Function or line number:** ~39–45  
* **Vulnerable code:** `Limit::perMinute(5)->by($email.$ip)`  
* **Risk explanation:** Good control; still bypassable via distributed IPs (Needs Manual Verification for WAF).  
* **Recommended fix:** Consider CAPTCHA after N failures.  
* **Secure replacement code:** Optional Fortify + Turnstile/hCaptcha.  
* **Verification steps:** 6th login/minute blocked.

---

### Finding title: AuthenticateSession + logout invalidate session

* **Severity:** Informational  
* **Confidence:** Confirmed  
* **Affected file:** `app/Http/Kernel.php`, Fortify logout  
* **Function or line number:** web middleware group  
* **Vulnerable code:** Jetstream `AuthenticateSession` enabled.  
* **Risk explanation:** Positive control against session reuse after password change.  
* **Recommended fix:** Keep.  
* **Secure replacement code:** N/A  
* **Verification steps:** Change password; other session logged out.

---

### Finding title: Leftover dd/var_dump in Livewire/admin

* **Severity:** Low  
* **Confidence:** Confirmed  
* **Affected file:** e.g. `app/Http/Livewire/Vehicles/Rop.php`, `EditFaq.php`, `Admin/Vehicles.php`  
* **Function or line number:** various  
* **Vulnerable code:** `dd` / `var_dump` left in code paths.  
* **Risk explanation:** DoS or info leak if hit.  
* **Recommended fix:** Remove all debug dumps.  
* **Secure replacement code:** Delete statements; use `Log::debug` behind debug flag.  
* **Verification steps:** Grep clean for `\bdd\(`.

---

### Finding title: Broken `/insurer-documnet/{id}` route

* **Severity:** Low  
* **Confidence:** Confirmed  
* **Affected file:** `routes/web.php`  
* **Function or line number:** ~98–99  
* **Vulnerable code:** Points to missing `FileUploadController` method.  
* **Risk explanation:** Error noise; latent risk if wired incorrectly later.  
* **Recommended fix:** Remove or implement with auth.  
* **Secure replacement code:** Delete route.  
* **Verification steps:** Route absent from `route:list`.

---

### Finding title: .env not tracked by git

* **Severity:** Informational  
* **Confidence:** Confirmed  
* **Affected file:** `.gitignore`  
* **Function or line number:** `.env` entry  
* **Vulnerable code:** N/A — positive.  
* **Risk explanation:** Secrets should stay out of VCS (verify history separately).  
* **Recommended fix:** Rotate any keys ever committed historically.  
* **Secure replacement code:** N/A  
* **Verification steps:** `git ls-files .env` empty; `git log --all -- .env` reviewed.

---

### Finding title: public/.htaccess disables indexes and forces HTTPS for production host

* **Severity:** Informational  
* **Confidence:** Confirmed  
* **Affected file:** `public/.htaccess`  
* **Function or line number:** `-Indexes`, HTTPS rewrite for isiboman.com  
* **Vulnerable code:** N/A — positive.  
* **Recommended fix:** Ensure HSTS header at reverse proxy.  
* **Secure replacement code:** `Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"`  
* **Verification steps:** Directory listing denied; HTTP→HTTPS.

---

### Finding title: No JWT implementation

* **Severity:** Informational  
* **Confidence:** Confirmed  
* **Affected file:** N/A  
* **Function or line number:** N/A  
* **Vulnerable code:** None — Sanctum session/tokens used.  
* **Risk explanation:** N/A  
* **Recommended fix:** N/A  
* **Secure replacement code:** N/A  
* **Verification steps:** N/A

---

### Finding title: Security headers (CSP/HSTS/XFO) not comprehensively set in app

* **Severity:** Low  
* **Confidence:** Needs Manual Verification (may be at CDN)  
* **Affected file:** Application middleware / server config  
* **Function or line number:** N/A in app  
* **Vulnerable code:** No Laravel middleware found for CSP/HSTS/Permissions-Policy.  
* **Risk explanation:** Clickjacking / XSS impact amplification if headers absent.  
* **Recommended fix:** Add security headers middleware or CDN rules.  
* **Secure replacement code:**

```php
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; script-src 'self'");
```

* **Verification steps:** securityheaders.com scan.

---

# Dependencies that need upgrading / audit posture

| Item | Status | Recommendation |
|------|--------|----------------|
| `composer audit --locked` at review time | No advisories reported | Re-run on each release |
| `composer.json` `audit.block-insecure: false` | Disabled | Set `true` in CI |
| `barryvdh/laravel-debugbar` | require-dev | Production: `composer install --no-dev` |
| Laravel framework / Livewire / Jetstream | On Laravel 13 stack | Keep on supported versions; subscribe to security releases |
| PHP | Target 8.4 | Keep patch level current |

**Recommended safe practice:** Pin exact versions in deploy; weekly `composer audit`; Dependabot/Renovate.

---

# Security configuration recommendations

1. `APP_ENV=production`, `APP_DEBUG=false` (config default false).  
2. `SESSION_SECURE_COOKIE=true`, `SESSION_SAME_SITE=lax` or `strict` for admin.  
3. `CACHE_PREFIX` / `SESSION_COOKIE` pinned (already `isib_*`).  
4. HTTPS only; HSTS at edge.  
5. CORS allowlist; no `*`.  
6. Disable Fortify registration; enable reset + 2FA for admins.  
7. Private file disk for customer uploads; signed URLs.  
8. Remove all `/test*` routes.  
9. Thawani webhook endpoint with signature verification.  
10. WAF rate limits on login, follow-up, upload, payment.  
11. `composer install --no-dev --optimize-autoloader` on deploy.  
12. Do not log tokens, passwords, full card/session payment payloads.

---

# Items requiring manual penetration testing

* End-to-end Thawani payment tampering (amount, replay, race).  
* Follow-up ID enumeration at scale + mail interception.  
* Upload malware/polyglot (Content-Type vs content).  
* Session fixation / hijacking over HTTP if Secure flag missing.  
* Livewire parameter tampering on admin components.  
* ROP/Dhofar SOAP injection with real endpoints.  
* Account enumeration on login/register error messages.  
* Privilege escalation after disabling registration (legacy accounts).  
* CDN/header presence (CSP/HSTS).  
* Confirm whether `session('file')` payment bypass path is reachable.

---

# Prioritized remediation plan

| Priority | Timeline | Actions |
|----------|----------|---------|
| **P0 — 24–48h** | Immediate | Remove `/test_`, `/testapi*`, `/testemail`; rotate Anoud Basic Auth and review all `.env` secrets; disable Fortify registration; hash `CreateUser` passwords; force reset for plaintext users |
| **P1 — 1 week** | Critical path | Fix `VeOffer` SQLi; IDOR on `viewCustomerDoc`; follow-up OTP + email-to-insured-only; admin role middleware |
| **P2 — 2 weeks** | Payments | Scope offer IDs to session; Thawani webhook + amount/`client_reference_id` checks; POST checkout; coupon re-validate |
| **P3 — 2–3 weeks** | Hardening | SOAP XML escape + TLS verify; FAQ XSS sanitization; upload private disk + max size; session Secure cookie; APP_DEBUG default false |
| **P4 — ongoing** | Defense in depth | Policies/Gates; 2FA; password complexity; CORS allowlist; security headers; re-enable composer audit blocking; remove `dd` |

---

# Final security checklist

- [ ] Public registration disabled or admin-role gated  
- [ ] `/admin/*` requires role/permission  
- [ ] All passwords hashed (Fortify + CreateUser)  
- [ ] No plaintext passwords in DB for existing users  
- [ ] `/test_` and diagnostic routes removed; credentials rotated  
- [ ] `VeOffer::OfferDetails` uses bindings / query builder  
- [ ] `viewCustomerDoc` ownership enforced  
- [ ] Follow-up not enumerable by raw ID  
- [ ] Policy emails only to insured address  
- [ ] Payment offers scoped to session request  
- [ ] Thawani amount + reference verified server-side  
- [ ] SOAP inputs XML-escaped; TLS verify on  
- [ ] FAQ output escaped/sanitized  
- [ ] `SESSION_SECURE_COOKIE=true` on HTTPS  
- [ ] `APP_DEBUG=false` in production  
- [ ] CORS origins allowlisted  
- [ ] Uploads size-limited on private disk  
- [ ] CSRF exceptions remain empty  
- [ ] `composer install --no-dev`; audit blocking enabled  
- [ ] Security headers verified in production  
- [ ] Manual pen-test of payment + follow-up completed  

---

## Summary for stakeholders

### Findings by severity

| Severity | Count |
|----------|-------|
| Critical | 7 |
| High | 14 (12 confirmed + 2 NMV) |
| Medium | 20 |
| Low / Informational | 11 |

### Five most urgent issues

1. Public registration → full admin  
2. IDOR on `/view-uploaded-file/{id}`  
3. Follow-up IDOR + document email chain  
4. SQL injection in `VeOffer::OfferDetails`  
5. Hardcoded Basic Auth on `/test_`  

### Files requiring immediate attention

`routes/web.php`, `FileUploadController.php`, `FrontendController.php`, `ThawaniPaymentController.php`, `VeOffer.php`, `CreateUser.php`, `VehicleInsurance.php`, `config/fortify.php`, session/`.env` cookie settings, FAQ Blade views.

### Recommended fix order

P0 credentials/routes/registration/password hashing → P1 SQLi/IDOR/authz → P2 payments → P3 SOAP/XSS/uploads/cookies → P4 policies/2FA/headers/audit.

---

## Developer playbook — step-by-step fixes

Follow these steps **in order**. Do not skip P0. After each step: run the app smoke test listed, commit, then continue.

**General rules for the developer**

1. Work on a branch (e.g. `fix/security-p0`).
2. Never commit real secrets; put them only in `.env` (already gitignored).
3. After changing routes/config: `php artisan route:clear && php artisan config:clear`.
4. After Blade/Livewire changes: hard-refresh browser / clear view cache if needed: `php artisan view:clear`.
5. Tick the matching item in **Final security checklist** when done.

---

### Step 1 (P0) — Remove public test / diagnostic routes

**Why:** `/test_` exposes hardcoded Basic Auth; `testapi` / `testemail` hit live SOAP/mail unauthenticated.

**Files:** `routes/web.php`, `app/Http/Controllers/FrontendController.php` (methods can stay private until deleted later)

**Steps:**

1. Open `routes/web.php`.
2. Delete the entire `Route::get('test_', function(){ ... });` block (approx. lines 8–60).
3. Delete or comment out:
   - `Route::get('testemail', ...)`
   - `Route::get('testapi', ...)`
   - `Route::get('testapi2', ...)`
4. Review `Route::get('insurances/rop', ...)` — if it is only for debugging, remove it too or wrap with `auth` + admin middleware.
5. Save, then run: `php artisan route:list` and confirm those URIs are gone.
6. **Ops (required):** Rotate the Anoud Basic Auth password at the provider (the old value was in source). Treat it as compromised.
7. Optionally delete dead methods `test_email`, `test_api`, `test_api2`, `dhofar_api` from `FrontendController` in a follow-up commit.

**Verify:** `GET /test_`, `/testapi`, `/testemail` return 404.

---

### Step 2 (P0) — Disable public registration

**Why:** Anyone who registers can open `/admin/*`.

**Files:** `config/fortify.php`

**Steps:**

1. Open `config/fortify.php`.
2. In `'features' => [...]`, comment out or remove `Features::registration(),`.
3. Keep profile/password update features as needed.
4. Run `php artisan config:clear`.
5. Hide/remove Register links in views if any still point to `/register`.

**Verify:** `/register` is unavailable (404 or disabled). Existing admins can still log in at `/login`.

---

### Step 3 (P0) — Add admin-only gate on `/admin`

**Why:** Even with registration off, every authenticated user must not be an admin.

**Files:** `app/Models/User.php`, migration (new), `app/Providers/AuthServiceProvider.php` (or `AppServiceProvider`), `routes/web.php`

**Steps:**

1. Create migration: add boolean `is_admin` default `false` on `users` (or use a `role` string column).
2. Mark known admin accounts in DB: `UPDATE users SET is_admin = 1 WHERE email IN (...);`
3. Define a Gate (example in `AppServiceProvider::boot`):

```php
Gate::define('access-admin', fn ($user) => (bool) $user->is_admin);
```

4. Change admin route group middleware from only `auth:sanctum` to:

```php
'middleware' => ['auth:sanctum', 'can:access-admin'],
```

5. Ensure Jetstream/Fortify home redirects non-admins away from `/admin`.

**Verify:** Non-admin user hitting `/admin` gets 403. Admin user can open dashboard.

---

### Step 4 (P0) — Hash passwords in admin CreateUser

**Why:** `User::create($validatedData)` stores plaintext passwords.

**Files:** `app/Http/Livewire/CreateUser.php`, `app/Models/User.php`

**Steps:**

1. In `CreateUser::store()`:
   - Validate as today.
   - `unset($validatedData['password_confirmation']);`
   - `$validatedData['password'] = Hash::make($validatedData['password']);`
   - Then `User::create($validatedData);`
2. Add `use Illuminate\Support\Facades\Hash;`
3. On `User` model, cast password as hashed (Laravel 10+ style):

```php
protected function casts(): array
{
    return [
        'password' => 'hashed',
        // keep existing casts
    ];
}
```

4. **Data fix:** Identify users whose password does not look like bcrypt (`$2y$...`). Force password reset or re-hash via a one-off artisan command for those accounts only (do not double-hash valid hashes).

**Verify:** Create a user in admin UI; DB password starts with `$2y$` or `$argon`; login with that password works.

---

### Step 5 (P1) — Fix SQL injection in `VeOffer::OfferDetails`

**Why:** `whereRaw("'$val' BETWEEN ...")` concatenates session `car_value`.

**Files:** `app/Models/VeOffer.php`

**Steps:**

1. Replace `OfferDetails()` with query-builder comparisons (cast to float):

```php
public function OfferDetails()
{
    $val = (float) session('car_value');

    return $this->hasMany(VehicleOfferDetail::class, 'vehicle_offer_id', 'id')
        ->where('min_value', '<=', $val)
        ->where('max_value', '>=', $val);
}
```

2. Ensure Livewire still sets `session('car_value')` as a numeric string only (validate `numeric|min:0` on input).

**Verify:** Quote flow still shows offers for a normal car value. Setting `car_value` to `1' OR '1'='1` must not return all rows or SQL errors that execute injection.

---

### Step 6 (P1) — Fix document view IDOR

**Why:** `viewCustomerDoc` has no ownership check (delete already does).

**Files:** `app/Http/Controllers/FileUploadController.php`, `routes/web.php`

**Steps:**

1. In `viewCustomerDoc($id)`, after `find`:
   - If `$query->request_id != session('request_id')` → `abort(404)`.
2. Remove duplicate public route definitions at the bottom of `web.php` (~251–260); keep a single set of upload routes.
3. (Recommended same PR) Change storage from `public` disk to `local` (private) and serve only via this controller.

**Verify:** Upload under session A. From a clean browser session, `GET /view-uploaded-file/{id}` returns 404. Same session A can still view.

---

### Step 7 (P1) — Harden follow-up lookup and document email

**Why:** Numeric request ID returns full PII; documents may email `session('request_email')` instead of insured email.

**Files:** `app/Http/Controllers/FrontendController.php`, `resources/views/frontend/follow.blade.php`, possibly new OTP table/mail

**Steps (minimum viable fix):**

1. **Email binding (quick win):** In `getFollowUpDocumet`, set `$email` from the insured row loaded from DB (joined `insureds.email`), **not** `session('request_email')`.
2. **Lookup hardening (required for Critical):** Do not return PII on ID alone. Implement one of:
   - Email OTP: user enters request ID + email → send 6-digit code → verify before showing data; or
   - Shared secret: require last 4 of phone + plate number match DB before showing data.
3. Rate-limit `POST /submit_follow` (e.g. 5/min per IP) via route middleware `throttle:5,1`.
4. Log failed lookups without logging full PII.

**Verify:** Wrong phone/OTP cannot see another customer’s follow-up. Policy email arrives only at insured DB email.

---

### Step 8 (P2) — Scope payment `offer_id` to current session request

**Why:** Client can pass another request’s cheaper offer ID.

**Files:** `app/Http/Controllers/FrontendController.php` (`process_payment`, `domestic_process_payment`, travel payment)

**Steps:**

1. Vehicle path: load offer with request scope, e.g.:

```php
$offer = VehicleRequestOffer::where('id', $request->offer_id)
    ->where('vehicle_request_id', session('vrequest')) // use actual FK column name
    ->firstOrFail();
```

2. Confirm the real FK column name in DB/migrations (`request_id` vs `vehicle_request_id`) and use that.
3. Repeat for domestic and travel (`selected_price` / travel offer IDs must belong to `session('request_id')` / travel request).
4. Re-validate coupon from DB at charge time (`active = 1`) instead of trusting `session('disscount')` alone.
5. Change payment initiation routes from **GET** to **POST** + `@csrf` on forms.

**Verify:** Session for request A + foreign `offer_id` → 404. Valid offer still creates Thawani session.

---

### Step 9 (P2) — Harden Thawani payment confirmation

**Why:** Browser/session marks `pay_status` without amount / reference checks.

**Files:** `app/Http/Controllers/ThawaniPaymentController.php`, optionally new webhook route

**Steps:**

1. In `get_session()`, after Thawani returns `paid`:
   - Assert `(string) $client_reference_id === (string) $order_id`.
   - Load `selected_offer` and compare charged amount to expected `(int) round($order->total_price * 1000)` (confirm Thawani field name for amount).
   - Store real gateway payment/transaction id if available (not only client_reference).
   - Skip update if already `paid` (idempotent).
2. Replace `return response()->json([$e], 200)` with `report($e)` + generic 500 JSON.
3. (Preferred) Add a Thawani webhook route that verifies signature/shared secret and updates pay status; treat browser return URL as UX only.
4. Do not mark paid solely because user hit `/payed`.

**Verify:** Mismatched `order_id` / amount does not set `pay_status=paid`. Happy path still works.

---

### Step 10 (P3) — SOAP XML escape + enable TLS verify

**Why:** User fields concatenated into XML; `'verify' => false` enables MITM.

**Files:** `app/Models/VehicleInsurance.php`, `app/Http/Controllers/FrontendController.php` (SOAP call sites)

**Steps:**

1. Add helper:

```php
private static function xmlEscape($value): string
{
    return htmlspecialchars((string) $value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}
```

2. Wrap every interpolated user/config value in SOAP bodies with `xmlEscape()` (license, plate, credentials from config, etc.).
3. Remove `'verify' => false` and `withoutVerifying()` so Laravel HTTP client verifies TLS.
4. If staging uses a private CA, configure `CURL_CA_BUNDLE` / php.ini `openssl.cafile` — do not disable verify in production.

**Verify:** Quote still works against real ROP/Dhofar. Request with `</civilId>` in plate is escaped in outbound XML (log temporarily in local only).

---

### Step 11 (P3) — Fix FAQ stored XSS

**Why:** `{!! $faq->answer !!}` / `html_entity_decode` executes HTML/JS.

**Files:** `resources/views/frontend/vehicles.blade.php`, `faqs.blade.php`, `travels.blade.php`, `domestic/worker.blade.php`, admin FAQ create/edit

**Steps:**

1. Prefer Blade escape: `{{ $faq->answer }}` for plain text.
2. If rich text is required: install/use HTMLPurifier (or equivalent) and allow only safe tags (`p`, `ul`, `li`, `strong`, `a`).
3. Stop using `html_entity_decode` + `echo` on FAQ answers.
4. Optionally strip tags on save in Livewire `CreateFaq` / `EditFaq`.

**Verify:** Saving `<script>alert(1)</script>` as FAQ answer does not execute on public pages.

---

### Step 12 (P3) — Upload hardening + session cookie / debug defaults

**Files:** `FileUploadController.php`, `config/filesystems.php`, `config/session.php`, `config/app.php`, `.env`, `.env.example`

**Steps:**

1. Validation: add `max:5120` (or agreed limit) and tighten mimes.
2. Store with random name: `$req->file('file')->store('uploads/customer', 'local');` (not public disk + original filename).
3. In `.env` / `.env.example`:

```env
SESSION_SECURE_COOKIE=true
APP_DEBUG=false
```

4. Change `config/app.php` default: `env('APP_DEBUG', false)`.
5. Change `config/session.php` default secure to true when appropriate: `env('SESSION_SECURE_COOKIE', true)`.

**Verify:** Cookie has `Secure` on HTTPS. Oversized upload rejected. Files not directly reachable under `/storage/uploads/...` without controller.

---

### Step 13 (P4) — CORS, Sanctum expiry, composer audit, headers, 2FA

**Files:** `config/cors.php`, `config/sanctum.php`, `composer.json`, `config/fortify.php`, new middleware optional

**Steps:**

1. CORS: replace `'allowed_origins' => ['*']` with production domain(s) only.
2. Sanctum: set `'expiration' => 60 * 24 * 7` (minutes) or policy value.
3. Composer: set `"audit": { "block-insecure": true }`; deploy with `composer install --no-dev`.
4. Enable Fortify `Features::resetPasswords()` and `Features::twoFactorAuthentication([...])` for admins; document enrollment.
5. Add security headers (app middleware or reverse proxy): `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, HSTS, CSP.
6. Grep and remove leftover `dd(` / `var_dump(` / `ddd(` in Livewire.
7. Register real Policies and call `$this->authorize()` in Livewire destroy/update.

**Verify:** `composer audit` clean; non-allowlisted origin blocked; 2FA enrollment works for admin; headers visible in browser Network tab.

---

### Step 14 — Final regression smoke (after P0–P3 at minimum)

Run manually on staging with PHP 8.4:

1. Home theme + quote form loads.  
2. Login as admin works; register disabled; non-admin blocked from `/admin`.  
3. Vehicle quote → offers → payment session create (sandbox).  
4. Upload document → view only in same session; other session 404.  
5. Follow-up requires OTP/secret; no PII on ID alone.  
6. FAQ page no script execution.  
7. `php artisan route:list` has no `test_` / `testapi`.  
8. Tick all items in **Final security checklist**.

---

*End of SECURITY_CHECK.md — assessment + developer fix playbook. Application code is not modified by this document alone.*
