Skip to content

Seguridad

Technomantus Corvi edited this page Sep 5, 2026 · 1 revision

Seguridad

Riesgo Mitigación
XSS Usar e() en todas las vistas
Inyección SQL Todos los métodos de Model usan prepared statements; los nombres de columna/tabla se validan contra un patrón estricto de identificador
Exposición de credenciales Las credenciales solo viven en .env (excluido de git)
Exposición de errores APP_DEBUG=false en producción
Exposición del código fuente Solo public/ es alcanzable por el servidor web; core/, controllers/, models/, config/ quedan fuera de la raíz de documentos
Listado de directorios Options -Indexes en public/.htaccess
Archivos sensibles (Nginx) deny all para .env, .git, .htaccess
IP de cliente falsificada Request::ip() confía en X-Forwarded-For, que cualquier cliente puede establecer — solo confía en ella detrás de un proxy inverso de confianza que sobrescriba esa cabecera
Almacenamiento de contraseñas password_hash()/password_verify() (bcrypt), nunca texto plano — ver Autenticación
Tokens de recuperación Guardados como hash SHA-256, de un solo uso (se eliminan tras usarse), expiran en 1 hora
Fijación de sesión session_regenerate_id(true) en login y logout
Enumeración de emails /forgot-password muestra el mismo mensaje exista o no el email registrado

Protección CSRF (opcional)

Tanuki no fuerza la verificación CSRF por defecto — el esqueleto base evita cualquier cosa que corra silenciosamente en cada petición sin que tú decidas activarla.

csrf_token()      // devuelve el token de la sesión actual (lo crea en la primera llamada)
csrf_field()      // renderiza un <input> oculto listo para usar
csrf_verify($tok) // comparación segura frente a timing attacks contra el token de sesión

1. Añade el campo a cualquier formulario que modifique datos:

<form method="POST" action="/todo">
    <?= csrf_field() ?>
    <!-- ... -->
</form>

2. Verifícalo al principio de la acción del controlador:

public function store(): void
{
    if (!csrf_verify($this->request->post('_token'))) {
        $this->flash('error', 'Tu sesión expiró. Inténtalo de nuevo.');
        $this->redirect('/todo/create');
    }
    // ...
}

Por qué esto no viene conectado por defecto en el router: forzarlo globalmente en cada ruta POST/PUT/DELETE obligaría a cualquier formulario de cualquier proyecto — incluyendo webhooks y endpoints tipo API que no usan sesiones — a llevar un token, lo necesite o no. Mantenerlo explícito por formulario deja el esqueleto base sin opiniones impuestas. El CRUD de TODO de ejemplo y las extensiones tanuki_login/tanuki_admin ya lo usan en cada formulario que muta datos — copia ese patrón para tus propios recursos.

Por qué public/ es la única carpeta expuesta

Todo excepto public/ queda fuera de la raíz de documentos del servidor web. core/, controllers/, models/ y config/ nunca pueden pedirse directamente por URL, sin importar cómo esté configurado el servidor. Mismo patrón que usan Laravel, Symfony y la mayoría de frameworks PHP modernos — ver Estructura del proyecto.

Clone this wiki locally