🛡️ Seguridad en PrestaShop

Guia completa de seguridad para el desarrollo de modulos: tokens CSRF, prevencion de SQL Injection, XSS, uploads seguros y cumplimiento OWASP Top 10.

🚨
Regla de oro

Nunca confies en datos que llegan del cliente. Valida, sanitiza y escapa SIEMPRE: Tools::getValue(), $_POST, $_GET, $_FILES.

Sistema de tokens CSRF

php
<?php
// En el Admin Controller — verificar token
if (!Tools::isSubmit('submit' . $this->name)) {
    return;
}

// Verificar token de admin
if (!Tools::getAdminToken(
    'AdminMyController' . (int)Tab::getIdFromClassName('AdminMyController') . (int)$this->context->employee->id
)) {
    die('Token invalido');
}

// Front office — token estatico (no requiere login)
$static_token = Tools::getToken(false);

// En Smarty asignar el token
$this->context->smarty->assign('static_token', Tools::getToken(false));

// En el template verificar en AJAX
// JavaScript: fetch('/module/mymodule/ajax?token=' + ps_static_token)

// PHP: verificar en el controlador AJAX
if (!Tools::getValue('token') || !Tools::isToken(
    Tools::getValue('token')
)) {
    die(json_encode(['error' => 'Token invalido']));
}

Prevencion de SQL Injection

php
<?php
// ❌ NUNCA HACER ESTO:
$id = Tools::getValue('id_product');
$sql = 'SELECT * FROM ps_product WHERE id_product = ' . $id; // VULNERABLE

// ✅ CORRECTO — casting a tipo esperado:
$id_product = (int) Tools::getValue('id_product');
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'product` WHERE id_product = ' . $id_product;

// ✅ CORRECTO — para strings usar pSQL():
$name = pSQL(Tools::getValue('name'));
$sql = "SELECT * FROM `ps_product_lang` WHERE name = '" . $name . "'";

// ✅ CORRECTO — para nombres de tablas/columnas usar bqSQL():
$table = bqSQL(Tools::getValue('table'));
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . $table . '`';

// Funciones de escape disponibles:
// pSQL($str)     → Escapa string para usar en queries
// (int)$val      → Casting a entero
// (float)$val    → Casting a float
// (bool)$val     → Casting a booleano
// bqSQL($str)    → Escapa nombre de tabla/columna (sin comillas)
// addslashes($s) → Solo para nombres de archivo, NO para queries

Prevencion de XSS

template.tpl
smarty
{* ❌ NUNCA mostrar variables sin escapar: *}
{$user_input}

{* ✅ SIEMPRE escapar variables de usuario: *}
{$user_input|escape:'html':'UTF-8'}

{* ✅ Para URLs: *}
<a href="{$url|escape:'url'}">Enlace</a>

{* ✅ HTML permitido (solo si confias en la fuente): *}
{$description nofilter}   {* No escapar — usar SOLO con contenido del admin *}
PHP
php
<?php
// En PHP, siempre escapar antes de mostrar:
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');

// Para contenido HTML (editor TinyMCE, etc):
echo strip_tags($html, '<p><b><i><strong><em><ul><ol><li><br><a>');

// Tools::purifyHTML() — usa HTMLPurifier internamente:
echo Tools::purifyHTML($html_content);

// Validar con los metodos de Validate:
if (!Validate::isCleanHtml($html)) {
    // Contiene codigo peligroso
}
if (!Validate::isName($name)) {
    // Nombre invalido
}

File Uploads seguros

php
<?php
// Subida de archivos segura en un modulo
if (isset($_FILES['my_file']) && $_FILES['my_file']['error'] === UPLOAD_ERR_OK) {
    $file = $_FILES['my_file'];

    // 1. Verificar tamano maximo
    $maxSize = 2 * 1024 * 1024; // 2MB
    if ($file['size'] > $maxSize) {
        throw new Exception('Archivo demasiado grande');
    }

    // 2. Verificar MIME type real (no confiar en extension)
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime  = $finfo->file($file['tmp_name']);
    $allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    if (!in_array($mime, $allowed, true)) {
        throw new Exception('Tipo de archivo no permitido');
    }

    // 3. Generar nombre aleatorio (NUNCA usar el nombre original)
    $ext      = pathinfo($file['name'], PATHINFO_EXTENSION);
    $ext      = strtolower(preg_replace('/[^a-z0-9]/', '', $ext));
    $filename = bin2hex(random_bytes(16)) . '.' . $ext;

    // 4. Mover al directorio seguro (fuera del webroot si es posible)
    $uploadDir = _PS_MODULE_DIR_ . $this->name . '/uploads/';
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0755, true);
        file_put_contents($uploadDir . 'index.php', '<?php header("Location: ../../../");');
    }

    move_uploaded_file($file['tmp_name'], $uploadDir . $filename);
}

OWASP Top 10 en PrestaShop

#VulnerabilidadPrevencion en PS
A01 Broken Access Control Validar permisos con `$this->context->employee->isLoggedBack()`, tokens de admin
A02 Cryptographic Failures Usar `Tools::passwdGen()` y `Tools::encrypt()`, no MD5 directo
A03 SQL Injection `pSQL()`, `(int)`, `bqSQL()`, nunca concatenar input sin escapar
A05 Security Misconfiguration Modo debug desactivado en produccion, errores no expuestos
A06 Vulnerable Components Mantener PS actualizado, auditar dependencias Composer
A07 Authentication Failures Usar sistema de auth de PS, no implementar autenticacion propia
A08 Software Integrity Failures Validar checksums de modulos de terceros antes de instalar
A10 SSRF Validar URLs antes de hacer peticiones cURL en el servidor