# Regla: Seguridad — PHP / Laravel

PHP es históricamente el lenguaje con más vectores de ataque web documentados.
Laravel mitiga la mayoría de ellos por defecto, pero solo si se usan sus
mecanismos correctamente. Esta regla es OBLIGATORIA sin excepciones.

---

## SQL Injection: Eloquent y Query Builder siempre

- NUNCA construir queries con interpolación de strings o concatenación:

```php
// MAL — vulnerable a SQL injection
$facturas = DB::select("SELECT * FROM facturas WHERE empresa_id = $empresaId");

// MAL — DB::raw sin binding
$facturas = Factura::whereRaw("empresa_id = $empresaId")->get();

// BIEN — Eloquent con binding automático
$facturas = Factura::where('empresa_id', $empresaId)->get();

// BIEN — DB::raw con bindings separados cuando es necesario
$facturas = Factura::whereRaw('empresa_id = ? AND total > ?', [$empresaId, $minimo])->get();
```

- Cada aparición de `DB::statement(` o `DB::unprepared(` requiere revisión de seguridad
  explícita con comentario documentando por qué es seguro.

---

## XSS: Blade escapa por defecto

- `{{ $variable }}` en Blade escapa HTML automáticamente. Usar siempre.
- `{!! $variable !!}` NO escapa. Prohibido salvo en casos específicos documentados:

```blade
{{-- MAL — XSS si $contenido viene del usuario --}}
{!! $contenido !!}

{{-- BIEN — escapado por defecto --}}
{{ $contenido }}

{{-- BIEN — HTML permitido solo de fuente confiable (CMS interno, no usuario) --}}
{{-- NOTA: $plantilla proviene de CMS interno administrado por el equipo, no de input de usuario --}}
{!! $plantilla->html_cuerpo !!}
```

- Si se necesita renderizar HTML de usuario (editor WYSIWYG), sanitizar con
  `HTMLPurifier` o `League\HTMLToMarkdown` antes de almacenar, nunca al mostrar.

---

## CSRF: middleware activo en todas las rutas web

- El middleware `VerifyCsrfToken` debe estar activo en el grupo `web`.
- No agregar rutas a `$except` en `VerifyCsrfToken` sin documentar explícitamente por qué.
- Las rutas de API que usan tokens de portador (Bearer) son inmunes a CSRF por diseño.
  Documentar esto en el controller si se desactiva el middleware para esa ruta.

---

## Mass Assignment: $fillable en todos los modelos

- Todo modelo Eloquent DEBE declarar `$fillable` o `$guarded`.
- Preferir `$fillable` (lista de permitidos) sobre `$guarded` (lista de bloqueados).
- NUNCA `protected $guarded = []` — permite asignación masiva sin restricción:

```php
// MAL — permite asignar cualquier campo incluyendo is_admin, empresa_id
class Usuario extends Model
{
    protected $guarded = [];
}

// BIEN — solo los campos que el usuario puede enviar
class Usuario extends Model
{
    protected $fillable = [
        'nombre',
        'email',
        'password',
    ];
    // empresa_id, is_admin, etc. solo se asignan explícitamente en el service
}
```

---

## Autenticación: Laravel Sanctum o Passport

- NUNCA implementar autenticación custom con hashing manual y generación de tokens.
- APIs de primera parte (SPA, mobile): **Laravel Sanctum**.
- OAuth2 para terceros: **Laravel Passport**.
- Configuración mínima de Sanctum para seguridad:

```php
// config/sanctum.php
'expiration' => 60 * 24,          // tokens expiran en 24 horas
'token_prefix' => env('APP_ENV'), // prefijo diferente por ambiente
```

- Los tokens de refresh van en cookies `httpOnly`, nunca en `localStorage`.

---

## Secretos en .env, nunca en config/

- Las credenciales se leen desde variables de entorno, nunca hardcodeadas:

```php
// MAL — credencial en código
class StripeService
{
    private string $apiKey = 'sk_live_abc123...';
}

// MAL — en archivo de config versionado
// config/servicios.php
return ['stripe' => ['key' => 'sk_live_abc123...']];

// BIEN — desde variable de entorno
return ['stripe' => ['key' => env('STRIPE_SECRET_KEY')]];
```

- Ejecutar `php artisan config:cache` antes de deploy para que `.env` no sea
  necesario en producción (las variables se inyectan por el sistema de deployment).

---

## PHPStan/Psalm en CI

- Nivel mínimo de análisis estático: **nivel 6** (PHPStan) o **nivel 3** (Psalm).
- Configurar en `phpstan.neon`:
  ```yaml
  parameters:
    level: 6
    paths:
      - app
    excludePaths:
      - app/Http/Middleware/TrustProxies.php
  ```
- CI falla si PHPStan reporta errores. Los errores ignorados se documentan
  con `// @phpstan-ignore-next-line` y comentario explicativo.

---

## Rate limiting en rutas de autenticación

```php
// routes/api.php
Route::middleware('throttle:5,1')->group(function () {
    Route::post('/login', [AuthController::class, 'login']);
    Route::post('/registro', [AuthController::class, 'registrar']);
    Route::post('/contrasena/recuperar', [AuthController::class, 'recuperarContrasena']);
});
```

- Máximo 5 intentos por minuto por IP en rutas de autenticación.
- Bloqueo temporal de 15 minutos al superar el límite.

---

## Headers de seguridad

- Agregar middleware con headers de seguridad en el grupo `web` y `api`:

```php
// Middleware SecurityHeaders::handle()
$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('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
```

---

## Checklist de seguridad antes de merge

- [ ] Sin interpolación de strings en queries SQL
- [ ] Sin `{!! !!}` en Blade sin comentario que justifique la fuente confiable
- [ ] `$fillable` declarado en todos los modelos nuevos
- [ ] Sin credenciales hardcodeadas (verificar con `git grep -rn "sk_live\|password.*=.*'[^']"`)
- [ ] Rutas de auth con `throttle` configurado
- [ ] PHPStan nivel 6 pasa sin errores en `app/`
- [ ] `composer audit` sin vulnerabilidades HIGH o CRITICAL
