# Security Remediation Design — Turista API

**Date:** 2026-06-23  
**Scope:** Fix the security audit findings from `docs/security-audit-2026-06-22.md` that are safe to change without the large `$fillable` backfill required by `Model::unguard()`.

## Goals

1. Close the High-severity IDOR gaps in units and reservations.
2. Close the Medium-severity configuration and leakage issues.
3. Close the Low-severity information-disclosure and hardening issues.
4. Keep the existing test suite green and add targeted regression tests.

## Issues addressed

| Severity | Finding | Fix approach |
|----------|---------|--------------|
| High | Bulk unit creation in any building | Add ownership check in `UnitController::bulkStore`. |
| High | Unit listing for any building | Already has a manual ownership check; harden by binding `Building $building` in `UnitController::index` so the check works against a real model. |
| High | Reservation `unit_id` changed to another owner's unit | Reject `unit_id` changes whose new unit has a different building owner in `ReservationService::updateReservation`. |
| High | Empty-scope availability leak | Always apply `whereIn('unit_id', $unitIds)` when `$unitIds !== null`, even if empty. |
| High | Default super-admin password | Stop creating a default super-admin in `DatabaseSeeder`; rely on a secure CLI/bootstrap path. |
| Medium | Guzzle CVEs | Run `composer update guzzlehttp/guzzle guzzlehttp/psr7` to patched versions. |
| Medium | Default CORS `*` | Publish `config/cors.php` and read origins from `CORS_ALLOWED_ORIGINS` with a localhost-only default. |
| Medium | Missing security headers | Add `SecurityHeadersMiddleware` and append it globally. |
| Medium | Debug/request-docs defaults | Set `APP_DEBUG=false` and `REQUEST_DOCS_ENABLED=false` in `.env.example`; uncomment `NotFoundWhenProduction` middleware. |
| Medium | Non-expiring Sanctum tokens | Set `expiration` in `config/sanctum.php` to a sensible TTL. |
| Medium | OTP leaked in local responses | Stop returning `otp` in auth and on-arrival responses; tests read OTP from the cache. |
| Medium | Login enumeration | Return the same generic 401 for unverified accounts as for bad credentials. |
| Low | `api.json` / `routes.json` committed | `git rm --cached` the files (they are already ignored). |
| Low | Incomplete password reset | Send a `PasswordResetMail` from `AuthController::forgotPassword`. |
| Low | Password hashes audited | Add `$auditExclude = ['password']` to `User`. |
| Low | Exception messages in responses | Replace `$e->getMessage()` with fixed safe messages. |
| Low | Inconsistent upload validation | Standardize image MIMEs and add `dimensions` / `max` rules on building/unit photo uploads. |

## Out of scope

- Removing global `Model::unguard()` is intentionally retained per `docs/REMAINING_FIXES.md`; doing it safely requires a `$fillable`/`$guarded` pass across every model.

## Verification

- Run `php artisan test` (PHPUnit/Pest).
- Run `vendor/bin/pint --test`.
- Run `composer audit` to confirm Guzzle advisories are gone.

## Risks

- Changing login response from 403 to 401 and removing OTP responses will require test updates.
- Adding `dimensions` to uploads requires test images to remain small enough.
- Publishing `config/cors.php` with a non-`*` default must still allow tests (tests rarely send an `Origin` header).
