Guía completa de seguridad en PHP: validación, uploads, SQL y sesiones

php Guía completa de seguridad en PHP: validación, uploads, SQL y sesiones

Guía completa de seguridad en PHP: validación, uploads, SQL y sesiones

Esta guía práctica cubre técnicas y patrones para proteger aplicaciones PHP: validación de entrada, manejo seguro de uploads, consultas con PDO/prepared statements, hardening de sesiones y CSRF. Incluye estructura de proyecto, código completo y explicaciones del por qué detrás de cada decisión.

Prerequisitos

  • PHP 7.4+ (idealmente 8.x)
  • Servidor web (Apache/Nginx)
  • Extensiones: PDO, fileinfo

Estructura de carpetas

project/
├─ public/              # Document root
│  ├─ index.php
│  └─ uploads/          # Carpeta de archivos subidos (bloquear ejecución)
├─ src/
│  ├─ config.php
│  ├─ db.php
│  ├─ auth.php
│  ├─ upload.php
│  └─ helpers.php
├─ logs/
├─ .env
└─ .htaccess            # reglas de seguridad para public/

1) Configuración y conexión segura a BD (PDO)

Usa PDO con opciones que obliguen a excepciones y deshabiliten emulación de prepared statements.

<?php
// src/db.php
declare(strict_types=1);

require_once __DIR__ . '/config.php';

function getPDO(): PDO
{
    static $pdo = null;
    if ($pdo !== null) {
        return $pdo;
    }

    $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_NAME);
    $options = [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ];

    $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
    return $pdo;
}

Por qué: deshabilitar emulación evita inyección when drivers try to interpolate values; las excepciones facilitan manejo centralizado de errores.

2) Manejo de contraseñas

<?php
// src/auth.php (fragmento)
function registerUser(string $email, string $password): bool
{
    $pdo = getPDO();
    $hash = password_hash($password, PASSWORD_DEFAULT);
    $stmt = $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (:email, :hash)');
    return $stmt->execute([':email' => $email, ':hash' => $hash]);
}

function verifyLogin(string $email, string $password): ?array
{
    $pdo = getPDO();
    $stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = :email LIMIT 1');
    $stmt->execute([':email' => $email]);
    $user = $stmt->fetch();
    if ($user && password_verify($password, $user['password_hash'])) {
        // opcional: password_needs_rehash
        if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
            $newHash = password_hash($password, PASSWORD_DEFAULT);
            $update = $pdo->prepare('UPDATE users SET password_hash = :h WHERE id = :id');
            $update->execute([':h' => $newHash, ':id' => $user['id']]);
        }
        return $user;
    }
    return null;
}

Por qué: password_hash + password_verify implementan salado y funciones de stretching correctamente; evita implementar tu propio esquema.

3) Validación y saneamiento de entrada

Separa validación (si el dato es válido) de saneamiento (transformación para uso). Nunca confíes en datos del cliente.

<?php
// src/helpers.php
function isValidEmail(string $email): bool
{
    return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
}

function sanitizeString(string $s): string
{
    return trim(filter_var($s, FILTER_SANITIZE_STRING)); // para HTML usar escape al imprimir
}

Al imprimir en HTML, usa htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') para prevenir XSS.

4) Manejo seguro de uploads

Pasos clave: validar tamaño y tipo MIME usando fileinfo, usar nombres aleatorios, almacenar fuera del document root o bloquear ejecución con .htaccess, restringir permisos.

<?php
// src/upload.php
function handleUpload(array $file, string $destDir): string
{
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Upload error code: ' . $file['error']);
    }

    // tamaño máximo
    $max = 5 * 1024 * 1024; // 5MB
    if ($file['size'] > $max) {
        throw new RuntimeException('File too large');
    }

    // comprobar tipo real con finfo
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file['tmp_name']);
    $allowed = [
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/gif' => 'gif',
    ];
    if (!isset($allowed[$mime])) {
        throw new RuntimeException('Tipo de archivo no permitido');
    }

    // nombre seguro
    $ext = $allowed[$mime];
    $basename = bin2hex(random_bytes(16));
    $filename = $basename . '.' . $ext;

    // mover a destino fuera del docroot si es posible
    $target = rtrim($destDir, '/') . '/' . $filename;
    if (!move_uploaded_file($file['tmp_name'], $target)) {
        throw new RuntimeException('No se pudo mover el archivo');
    }

    // establecer permisos restrictivos
    chmod($target, 0640);

    return $filename;
}

Complementa con validación adicional: getimagesize para comprobar imágenes y procesar/reescalar para prevenir imágenes maliciosas.

.htaccess para uploads (Apache)

# public/uploads/.htaccess
# Previene ejecución de scripts
Options -ExecCGI -Indexes
AddType text/plain .php .php5 .phtml .pl .py .jsp
php_flag engine off

Si no usas Apache, configura Nginx para no interpretar scripts en la carpeta de uploads.

5) CSRF y seguridad de sesiones

Genera tokens por sesión y valida en formularios; fortalece cookies de sesión.

<?php
// src/session.php
function secureSessionStart(): void
{
    $cookieParams = session_get_cookie_params();
    session_set_cookie_params([
        'lifetime' => 0,
        'path' => $cookieParams['path'],
        'domain' => $cookieParams['domain'],
        'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
}

function csrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function validateCsrf(string $token): bool
{
    return hash_equals($_SESSION['csrf_token'] ?? '', $token);
}

Además, regenera el ID de sesión al iniciar sesión (session_regenerate_id(true)) y limita la duración de la sesión con control de actividad.

6) Manejo de errores y logging

No muestres errores de PHP en producción. Registra errores con un sistema central (Monolog recomendado) y muestra mensajes genéricos al usuario.

// src/config.php (fragmento)
const ENV = 'production'; // o 'development'
if (ENV === 'production') {
    ini_set('display_errors', '0');
    ini_set('log_errors', '1');
    ini_set('error_log', __DIR__ . '/../logs/php-errors.log');
} else {
    ini_set('display_errors', '1');
    ini_set('display_startup_errors', '1');
}

Por qué: evitar fugas de información sensible (stack traces, consultas SQL) al usuario final.

7) Configuración y secretos

Guarda credenciales fuera del repositorio en variables de entorno (.env) y no en el código. Usa libs como vlucas/phpdotenv para cargar en desarrollo y el gestor de entorno del hosting en producción.

# .env ejemplo
DB_HOST=127.0.0.1
DB_NAME=myapp
DB_USER=myuser
DB_PASS=s3cret
APP_ENV=production

8) Reglas adicionales y hardening

  • Usa Content Security Policy (CSP) para reducir XSS.
  • Valida el tamaño y tipo de entrada en servidor (nunca solo JS).
  • Implementa rate limiting y bloqueo por IP para endpoints sensibles.
  • Escanea dependencias y aplica actualizaciones de seguridad.
  • Realiza pruebas de penetración y revisiones de código (SAST).

9) Ejemplo completo: registro con upload de avatar

Flujo resumido:

  1. secureSessionStart()
  2. Comprobar CSRF
  3. Validar email y password
  4. Procesar upload con handleUpload()
  5. Guardar user y nombre de archivo en BD con prepared statement
<?php
// public/index.php (simplificado)
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/session.php';
require_once __DIR__ . '/../src/helpers.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/upload.php';
require_once __DIR__ . '/../src/auth.php';

secureSessionStart();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!validateCsrf($_POST['csrf'] ?? '')) {
        http_response_code(400);
        echo 'Invalid CSRF token';
        exit;
    }

    $email = $_POST['email'] ?? '';
    $password = $_POST['password'] ?? '';

    if (!isValidEmail($email) || strlen($password) < 8) {
        echo 'Datos inválidos';
        exit;
    }

    try {
        $avatarFilename = null;
        if (!empty($_FILES['avatar']['name'])) {
            $avatarFilename = handleUpload($_FILES['avatar'], __DIR__ . '/../public/uploads');
        }

        $pdo = getPDO();
        $stmt = $pdo->prepare('INSERT INTO users (email, password_hash, avatar) VALUES (:email, :hash, :avatar)');
        $stmt->execute([
            ':email' => $email,
            ':hash' => password_hash($password, PASSWORD_DEFAULT),
            ':avatar' => $avatarFilename,
        ]);

        echo 'Registrado';
    } catch (Throwable $e) {
        error_log($e->getMessage());
        echo 'Ocurrió un error';
    }
}

// Formulario mínimo
$csrf = csrfToken();
?>

<form method="post" enctype="multipart/form-data">
  <input type="hidden" name="csrf" value="<?php echo htmlspecialchars($csrf, ENT_QUOTES, 'UTF-8'); ?>" />
  <input type="email" name="email" required />
  <input type="password" name="password" minlength="8" required />
  <input type="file" name="avatar" accept="image/*" />
  <button type="submit">Registrar</button>
</form>

Recap rápido de por qué estas prácticas importan

  • Prepared statements: evitan inyección SQL.
  • Validación + fileinfo: previenen uploads maliciosos.
  • password_hash: almacenamiento seguro de credenciales.
  • Session cookie flags + CSRF token: reducen secuestro de sesión y ataques CSRF.
  • No mostrar errores en producción: previene fuga de información sensible.

Consejo avanzado: integra una suite de pruebas de seguridad (SAST + DAST) en tu CI/CD y automatiza escaneos de dependencias. Una regla práctica: cualquier entrada que llegue de fuera pasa por validación estricta y, si es un archivo, por comprobaciones de tipo y re-procesado (resizing/normalización) antes de cualquier uso.

Advertencia: la seguridad es una capa continua. Aplica parches, revisa logs y mantén principios de mínimo privilegio en base de datos y sistema de archivos. Siguiente paso: instrumenta rate limiting y logging estructurado (JSON) para correlacionar intentos maliciosos.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión