# CEPI LMS — Security

> Verified from codebase. Last updated: 2026-06-02.

---

## 1. Authentication Security

### Implemented
- **Password hashing**: bcrypt with 12 rounds (`BCRYPT_ROUNDS=12`)
- **Session-based auth** with database session driver; session ID regenerated on login
- **Remember token** supported
- **CSRF protection**: Laravel default on all POST/PUT/DELETE form routes
- **Email verification**: Custom 6-digit code with 2-day expiry (stored on users table, not URL-based)
- **Rate limiting**: `throttle:6,1` on email verification and password reset endpoints; `throttle:10,1` on checkout routes
- **Google OAuth**: Socialite redirect/callback pattern; no secrets in frontend
- **Password reset**: Standard Laravel token-based reset flow

### Gaps
- No brute-force protection on the main login form beyond Laravel's default throttle
- No `throttle` middleware explicitly on `/login` POST in `auth.php` — relies on Breeze default
- Session encryption is disabled (`SESSION_ENCRYPT=false`) — consider enabling in production

---

## 2. Authorization (Role-Based Access Control)

### Implemented
- **Spatie laravel-permission** with role-based route middleware
- Route groups:
  - `role:admin` → `/admin/*`
  - `role:instructor` → `/instructor/*`
  - `auth + verified` → student lesson and checkout routes
- Dashboard dispatch uses `user_type` column to avoid repeated role lookups

### Enrollment-Level Access Control
- `Student\LessonController::verifyEnrollment()` validates:
  - `enrollment.status = active`
  - `expires_at IS NULL OR expires_at > now()`
  - Aborts 403 if no valid enrollment found
- **Lesson-to-course ownership** check: `$lesson->module->course_id !== $course->id` → 404
- This prevents accessing a lesson from a different course even if the IDs are known

### Admin Ownership Rules
- Instructors access only their own courses (`instructor_id = auth()->id()`)
- Admin has no such restriction — can manage all content

### Gaps
- No `Policy` classes defined — authorization logic is inline in controllers
- `Admin\CourseController` should verify the instructor is actually the owner before allowing edit (currently admin can edit any course, which is intentional, but instructor edit needs checking)
- No authorization check preventing a student from calling `Admin\` routes via direct URL if the `role:admin` middleware were misconfigured

---

## 3. Device Binding Security

### Implemented
- Server-side fingerprint: `SHA256(userAgent + acceptLanguage)` — cannot be spoofed from JS
- Trusted token: 80-char random string stored as `HMAC-SHA256(token, APP_KEY)` — cookie-bound
- Cookie flags: `HttpOnly=true`, `SameSite=Lax`, `Secure` when on HTTPS
- One active device per account; new device triggers 24-hour cooldown
- Admin can reset devices via `UserController::resetDevices()`
- `acknowledged_at` tracks when user accepted the device policy notice

### Gaps
- Device fingerprint is UA + accept-language only — not truly unique; could collide across similar browsers
- No IP-based anomaly detection
- Cooldown does not apply to admin accounts (by design, but should be documented)

---

## 4. Bunny Video / DRM Security

### Implemented
- **Signed embed URL**: `SHA256(tokenKey + videoId + expires)` — expires in 300s (configurable)
- **Signed HLS URL**: HMAC-SHA256 with `token_path` binding — prevents URL reuse on different paths
- Bunny credentials (`BUNNY_TOKEN_KEY`, `BUNNY_API_KEY`) stored only in `.env`
- Embed URL generation server-side; client never sees raw credentials
- TTL enforcement means captured URLs expire quickly

### Gaps
- Signed embed URL is delivered as an iframe `src` — it can technically be shared/screenshotted but not directly accessed as raw file
- No watermarking implemented
- No geo-restriction configured (would be done at Bunny CDN level)
- `is_preview` lessons are visible publicly — ensure no signed URL is generated for preview lessons without enrollment check (current code only generates signed URL in `Student\LessonController` which is behind `auth`)

---

## 5. Payment Security

### Bank Transfer (Live)
- Unique request code generated per payment (`BT-YYYYMMDD-XXXXXX`)
- Admin review required before enrollment is activated
- Admin notes stored on payment record
- Duplicate detection: reuses existing pending payment record for same user+course

### Gateway Payments (Skeleton — Incomplete)
- **JazzCash**: `verifyPayment()` checks `pp_ResponseCode` only — **no hash/HMAC verification implemented yet** (critical security gap)
- **Stripe**: `verifyPayment()` always returns `true` — **completely insecure placeholder**
- Webhook endpoint `POST /webhooks/payments/{gateway}` is **publicly accessible without signature verification** — this is a critical risk for production

### Critical Payment Security Gaps
1. JazzCash webhook/callback must verify `pp_SecureHash` before trusting any payment data
2. Stripe webhook must verify `Stripe-Signature` header using `stripe/stripe-php` SDK
3. `CheckoutController::callback()` does not create enrollment on success — the flow is incomplete
4. The `gateway` parameter in the callback URL (`?gateway=stripe`) can be manipulated by the user

---

## 6. API Security (Future — Required for Mobile)

When building the REST API, enforce:
- **Laravel Sanctum** for token-based authentication (install before any API work)
- Tokens scoped by ability: `read`, `write`, `admin`
- Token expiry (Sanctum supports `expiration` config)
- All API routes behind `auth:sanctum` middleware
- Rate limiting: `throttle:60,1` minimum on all API routes; stricter on auth endpoints
- Input validation via FormRequest classes on all API endpoints
- No sensitive fields (password hash, device tokens, Bunny credentials) in API responses
- `api_only` user guard separate from web guard

---

## 7. Input Validation

### Implemented
- Login, registration, password reset — standard Breeze validation
- Manual checkout — validates payer name, phone, reference, notes
- Admin enrollment — validates status, expires_at, user/course existence
- Lesson progress — validates boolean, integer, min:0 for all numeric fields

### Gaps
- Many admin CRUD controllers use inline `$request->validate()` without FormRequest classes
- No file upload size/type enforcement visible in `AdminMediaService` review (need to verify)
- API endpoints (future) must use FormRequest with strict type validation

---

## 8. XSS / Injection Protection

### Implemented
- Blade `{{ }}` auto-escaping prevents XSS in rendered views
- Laravel Query Builder / Eloquent prevents SQL injection
- CSRF tokens on all state-changing form routes (default Laravel)

### Gaps
- `article_content` (longtext) in lessons could contain raw HTML — if rendered unescaped with `{!! !!}`, this is an XSS vector. Verify that admin-entered HTML content is sanitized or restricted to trusted admins only
- Blog `detail` (longtext) field carries same risk

---

## 9. Sensitive Data Handling

- Passwords: bcrypt hashed, `Hidden` attribute on User model
- Device tokens: stored as HMAC hashes, never raw values
- Bunny credentials: `.env` only
- Payment credentials: `.env` only
- Bank account details: stored in `settings` table — ensure DB access is restricted

---

## 10. Production Security Checklist

- [ ] Set `APP_DEBUG=false` in production
- [ ] Set `SESSION_ENCRYPT=true`
- [ ] Configure `HTTPS` and set `SESSION_SECURE_COOKIE=true`
- [ ] Set `APP_ENV=production`
- [ ] Configure proper `MAIL_MAILER` (not `log`)
- [ ] Add rate limiting to login route
- [ ] Implement JazzCash secure hash verification before going live
- [ ] Implement Stripe webhook signature verification before going live
- [ ] Sanitize HTML content in lesson `article_content` and blog `detail`
- [ ] Restrict admin panel to known IP ranges or VPN (optional but recommended)
- [ ] Enable Laravel audit logging consistently across all admin actions
- [ ] Set strong `APP_KEY` and rotate if compromised
- [ ] Verify Bunny CDN has Referer locking configured (restrict to your domain)
