# ECOLE ECOIN Full Security & Quality Audit

Date: 2026-06-16
Auditor: Codex
Scope: Laravel 12 application, security posture, architecture quality, operational readiness, testing baseline, privacy handling, and integration boundaries.

## Executive Summary

The project shows strong product depth and good intent around RBAC, audit trails, signed URLs, encryption of selected PII, and event-driven integrations. The codebase is no longer a simple training platform; it is approaching an operational ERP-style system with admissions, finance, attendance, CRM, messaging, legacy import, events, and portals.

However, the current baseline is not yet at a clean modern-production standard. The highest concerns are:

1. Outdated dependencies with known advisories, including a Laravel framework advisory.
2. A stored-XSS risk in CMS rich-content blocks due to raw rendering without sanitization on save.
3. Legacy direct messaging services that still bypass the newer messaging architecture and log sensitive operational data.
4. Telegram webhook security that becomes weak when the secret is not configured, and the default `.env.example` leaves it empty.
5. Operational drift: pending migrations, very large dirty working tree, and verification commands that do not currently give a clean green baseline.

Overall assessment:

- Security maturity: Medium
- Architecture maturity: Medium to High
- Operational readiness: Medium
- Verification confidence: Medium-Low

## Evidence Collected

The following checks were executed:

- `php artisan about`
- `php artisan route:list --json`
- `php artisan app:architecture-check`
- `composer validate`
- `composer audit`
- `php artisan migrate:status`
- `php artisan schedule:list`
- targeted tests:
  - `php artisan test tests/Feature/Telegram/TelegramPlatformTest.php --filter="test_password_reset_request_generates_signed_url_only"`
  - `php artisan test tests/Feature/HomeVisibilityRegressionTest.php --filter="published_course_and_scheduled_upcoming_cohort_appear_on_home"`
  - `php artisan test tests/Feature/Registration/CohortAvailabilityRegressionTest.php`

Observed outcomes:

- `composer audit` reported 14 advisories affecting 9 packages.
- `app:architecture-check` reported 22 baseline findings.
- local database has pending migrations for legacy import and telegram platform tables.
- one targeted registration regression test failed because MySQL returned `Table definition has changed, please retry transaction`.
- `pint --test` and a full `php artisan test --stop-on-failure` did not complete within the audit timeout window.

## High Risk Findings

### 1. Dependency security baseline is outdated

Risk: High

Evidence:

- `composer audit` reported:
  - `laravel/framework` affected below `12.60.0`
  - `symfony/mime` high severity advisory
  - `guzzlehttp/psr7`, `symfony/http-foundation`, `symfony/http-kernel`, `symfony/routing`, and others with medium/low advisories

Impact:

- You currently depend on versions with published security advisories.
- Even if not all are directly exploitable in your exact flows, this blocks a strong production security posture.

Recommendation:

- Upgrade `laravel/framework` to at least `12.60.0`.
- Upgrade transitive Symfony and Guzzle packages by running a controlled Composer update and then re-running the full verification suite.
- Replace wildcard package constraints such as `barryvdh/laravel-dompdf: "*"` with a bounded version.

### 2. CMS rich-content blocks can be rendered raw without sanitization on save

Risk: High

Evidence:

- `app/Application/UseCases/Admin/Cms/UpsertCmsBlock.php`
  - writes `$blockData['data']` directly to storage
- `resources/views/components/cms/rich_content.blade.php`
  - renders `{!! $content !!}`
- unlike blog posts, CMS blocks are not passed through `HtmlSanitizerService`

Impact:

- Any actor with CMS edit rights can inject arbitrary HTML.
- If script payloads or malicious attributes slip through block data, this becomes stored XSS against public visitors or internal staff.

Recommendation:

- Introduce a CMS block sanitizer step before persistence.
- Define per-block schema rules and sanitize rich HTML fields explicitly.
- Prefer a stricter allowlist than generic raw HTML storage.

### 3. Legacy direct messaging services bypass the hardened messaging layer

Risk: High

Evidence:

- `app/Application/Services/SendRegistrationNotification.php`
- `app/Services/Notifications/TelegramService.php`

Problems observed:

- direct use of `SiteSetting::all()->pluck('value', 'key')`
- direct HTTP calls instead of queued outbox-driven flows
- direct logging of recipient phone or message body
- no clear idempotency or delivery abstraction

Impact:

- Architectural boundary drift
- inconsistent secret handling
- higher chance of duplicate sends, inconsistent audit, and PII leakage through logs

Recommendation:

- Freeze these legacy services and route all outbound messaging through the new messaging domain.
- Mark them deprecated and replace remaining call sites.
- Ensure all provider tokens are accessed through encrypted settings abstractions only.

## Medium Risk Findings

### 4. Telegram webhook authenticity becomes optional when secret is empty

Risk: Medium

Evidence:

- `app/Http/Controllers/Webhooks/TelegramWebhookController.php`
  - `assertSecret()` returns early if webhook secret is blank
- `.env.example`
  - `TELEGRAM_WEBHOOK_SECRET=` is empty by default

Impact:

- If production is deployed without `TELEGRAM_WEBHOOK_SECRET`, any actor can POST fake Telegram updates to the webhook.
- The route is throttled, but throttling is not an authenticity control.

Recommendation:

- Make the secret mandatory outside local environments.
- Fail fast at boot or on webhook registration when the secret is empty in non-local environments.
- Add an operational health check that warns if Telegram webhook secret is missing.

### 5. Admin student password reset still exposes generated plaintext passwords in UI

Risk: Medium

Evidence:

- `app/Services/PasswordResetService.php`
  - generates and returns plaintext password
- `app/Livewire/Admin/Students/AdminStudentsPage.php`
  - stores returned password in component state
- `resources/views/livewire/admin/students/admin-students-page.blade.php`
  - displays generated password to admin

Impact:

- Passwords are intentionally revealed to staff in plaintext.
- This increases shoulder-surfing, screenshot, browser-state, and operator-handling risk.

Recommendation:

- Prefer reset-link driven recovery instead of generated passwords.
- If manual reset is required, allow admin-set passwords only, avoid generated-display mode, and force user rotation on next login.

### 6. Tracking endpoint stores direct email and phone analytics data

Risk: Medium

Evidence:

- `app/Http/Controllers/TrackingController.php`
- `app/Support/Tracking/TrackingManager.php`

Impact:

- Public/browser-originated tracking can persist raw `email` and `phone_e164` into `tracking_events`.
- This may exceed data-minimization expectations for analytics and marketing tracking.

Recommendation:

- Hash or tokenize identities in tracking storage.
- Store direct PII only when there is a documented legal basis and a tightly scoped retention policy.
- Split business lead attribution from generic analytics telemetry.

### 7. Telescope is enabled by default and still records data in non-local environments

Risk: Medium

Evidence:

- `config/telescope.php`
  - `enabled => env('TELESCOPE_ENABLED', true)`
- `app/Providers/TelescopeServiceProvider.php`
  - access gate denies users by default, but collection remains enabled

Impact:

- Even if the UI is inaccessible, Telescope can still store exceptions, failed requests, failed jobs, and monitored-tag data in production.
- Sensitive values may still be retained depending on payload shape.

Recommendation:

- Default `TELESCOPE_ENABLED=false` outside local/staging.
- Tighten hidden headers and request parameters beyond only CSRF and cookie values.
- Document a production policy for Telescope retention and activation.

### 8. Preview access middleware writes a dedicated debug log on every request

Risk: Medium

Evidence:

- `app/Http/Middleware/EnsurePreviewAccess.php`
  - writes to `storage/logs/preview_debug.log` for every request

Impact:

- unnecessary request-volume logging
- performance noise
- path/access-state leakage into logs

Recommendation:

- Remove unconditional debug logging.
- Guard it behind a dedicated feature flag or local-only condition.

### 9. Pending migrations indicate local schema drift from application code

Risk: Medium

Evidence:

- `php artisan migrate:status` showed these pending:
  - `2026_05_24_140000_create_legacy_import_tables`
  - `2026_05_24_210000_create_legacy_completed_trainings_table`
  - `2026_05_24_230000_create_legacy_imported_enrollments_table`
  - `2026_05_26_090000_create_telegram_platform_tables`

Impact:

- local audit and test confidence is reduced because code and schema are not fully aligned
- features may appear broken or partially broken for reasons unrelated to business logic

Recommendation:

- maintain a clean migration baseline in development and CI
- add a CI gate that fails when migrations are pending in the test environment

## Low Risk Findings

### 10. `.env.example` defaults are too permissive for production-minded onboarding

Risk: Low

Evidence:

- `.env.example`
  - `APP_DEBUG=true`
  - empty `TELEGRAM_WEBHOOK_SECRET`
  - no explicit `TELESCOPE_ENABLED=false`

Recommendation:

- keep local convenience, but comment production-safe examples directly in the file
- add a deployment checklist and environment validation command

### 11. Logging sanitization is only partially reassuring

Risk: Low to Medium

Evidence:

- `config/logging.php`
  - `single` channel uses `PiiLogTap`
  - `daily` does not
- `app/Services/Security/PiiLogTap.php`
- `app/Providers/PiiServiceProvider.php`
  - appears unfinished and not the main enforcement point

Impact:

- sanitization depends on which logging channel is active
- custom `Log::build()` flows may bypass the standard tap expectations

Recommendation:

- apply the sanitizing processor consistently across all active channels
- remove the incomplete provider or finish it clearly

### 12. Verification pipeline is not giving a clean baseline yet

Risk: Low to Medium

Evidence:

- `vendor/bin/pint --test` timed out during audit window
- `php artisan test --stop-on-failure` timed out during audit window
- targeted test failed once with MySQL table-definition-change error

Impact:

- difficult to assert release confidence quickly
- higher regression risk during fast-moving changes

Recommendation:

- split CI into fast and full suites
- stabilize the test database lifecycle
- add deterministic smoke suites for critical flows

## Architecture Findings

### 13. Architecture checker confirms cross-domain drift in WhatsApp flows

Risk: Medium

Evidence:

- `php artisan app:architecture-check`
- findings:
  - `whatsapp-direct-write` in `app/Domain/WhatsApp/WhatsAppEnrollmentJourneyService.php`
  - multiple `livewire-tracking-side-effect` findings in registration and student flows

Impact:

- side effects are still leaking into UI orchestration and direct model writes
- long-term maintainability and invariants become harder to protect

Recommendation:

- move state mutations behind application actions/use cases
- emit facts/events from UI layers rather than orchestrating analytics directly in Livewire screens

## Positive Findings

The project also has meaningful strengths:

- broad route protection and policy usage are present across many sensitive areas
- signed URLs are used for finance receipt access and Telegram password reset flows
- user PII has an encryption trait and blind-index query support
- audit logging exists across multiple business actions
- custom architecture checking exists, which is a strong engineering maturity signal
- scheduler is present and active for billing, events, WhatsApp, and cohort automation

## Verification Snapshot

Successful targeted checks:

- Telegram signed reset URL generation test passed
- home visibility regression test passed

Unstable or incomplete checks:

- one registration regression test failed due local MySQL table-definition instability, not an application assertion failure
- full lint/test baseline was not completed within the audit run window

## Recommended Priority Plan

### Immediate

1. Upgrade framework and vulnerable dependencies.
2. Add sanitization to CMS block persistence and re-audit all raw HTML render paths.
3. Require Telegram webhook secret in non-local environments.
4. Stop exposing generated plaintext student passwords in admin UI.
5. Disable or heavily restrict Telescope outside local/staging.

### Short Term

1. Retire legacy direct Telegram and WhatsApp sender services.
2. Normalize logging sanitization across all channels and remove noisy debug logging.
3. Reduce direct PII in analytics tracking.
4. Align local/test schema with pending migrations.

### Medium Term

1. Clean architecture-check baseline findings.
2. stabilize CI with fast smoke tests plus a full nightly suite
3. document environment hardening, secret requirements, and operations runbooks

## ERP Assessment

From a systems perspective, the platform is beyond a simple LMS or admissions site. With finance, attendance, CRM, messaging, legacy migration, events, portals, security center, and corporate workflows, it is reasonable to describe it as an ERP-like education operations platform.

More precisely:

- not yet a fully hardened enterprise ERP
- but clearly an integrated operations platform with ERP characteristics

That means governance, release discipline, security baselines, and architecture boundaries now matter much more than in an ordinary Laravel app.
