La configuración por defecto de PHP no está pensada para producción: muestra errores en pantalla, no limita la memoria de forma agresiva y tiene funciones habilitadas que pueden suponer riesgos. Con unos pocos ajustes en php.ini se pasa de un servidor PHP "que funciona" a uno que resiste en producción real.
Estructura de los ficheros de configuración
# En Debian/Ubuntu con PHP 8.x:
/etc/php/8.x/fpm/php.ini # PHP-FPM
/etc/php/8.x/cli/php.ini # PHP CLI
/etc/php/8.x/fpm/conf.d/ # Ficheros de extensiones (incluidos automáticamente)
# Editar y aplicar:
sudo nano /etc/php/8.x/fpm/php.ini
sudo systemctl reload php8.x-fpm
# Verificar configuración activa:
php-fpm8.x -T # test de configuración
php --ini # ruta del php.ini activo
php -r "phpinfo();" # ver todos los valores
Errores: silenciar en pantalla, registrar en log
; php.ini para producción
display_errors = Off ; nunca mostrar errores al usuario
display_startup_errors = Off ; tampoco errores de arranque
log_errors = On ; sí registrar en el log
error_reporting = E_ALL ; registrar todos los errores (incluidos E_NOTICE, E_DEPRECATED)
error_log = /var/log/php/error.log
; En desarrollo:
; display_errors = On
; error_reporting = E_ALL
<?php
// Sobrescribir desde código (útil en entornos con un php.ini compartido)
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/miapp/php_errors.log');
error_reporting(E_ALL);
Sesiones seguras
; php.ini
session.cookie_httponly = On ; la cookie no es accesible desde JavaScript
session.cookie_secure = On ; solo enviar por HTTPS
session.cookie_samesite = Strict ; previene CSRF via cookies
session.use_strict_mode = On ; rechaza IDs de sesión no generados por el servidor
session.use_only_cookies = On ; no aceptar session_id en la URL (?PHPSESSID=...)
session.gc_maxlifetime = 1800 ; segundos hasta que una sesión se considera basura
session.name = SESSID ; cambia el nombre por defecto (fingerprinting)
<?php
// Regenerar ID después de login para prevenir session fixation
session_start();
session_regenerate_id(true); // true: elimina la sesión anterior
// Añadir validación adicional
$_SESSION['user_agent'] = hash('sha256', $_SERVER['HTTP_USER_AGENT']);
// Al inicio de cada petición:
if ($_SESSION['user_agent'] !== hash('sha256', $_SERVER['HTTP_USER_AGENT'])) {
session_destroy();
header('Location: /login');
exit;
}
Límites de memoria y tiempo
; php.ini
memory_limit = 256M ; ajusta según las necesidades de la app
max_execution_time = 30 ; segundos máximos por petición web
max_input_time = 60 ; tiempo máximo para parsear input
max_input_vars = 1000 ; límite de variables de entrada (previene ataques DoS)
; Para scripts CLI de larga duración (imports, exports...)
; ejecuta: php -d memory_limit=1G -d max_execution_time=0 import.php
; Subida de ficheros
file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M ; debe ser mayor que upload_max_filesize
max_file_uploads = 5
Deshabilitar funciones peligrosas
; php.ini
; Funciones que permiten ejecutar comandos del sistema operativo
; Desactívalas si tu aplicación no las necesita
disable_functions = exec,passthru,shell_exec,system,popen,proc_open,
pcntl_exec,pcntl_fork,proc_nice,proc_terminate,
show_source,phpinfo
; expose_php: oculta la versión de PHP en las cabeceras HTTP (X-Powered-By)
expose_php = Off
; allow_url_fopen y allow_url_include: solo si las necesitas
allow_url_fopen = Off ; deshabilita file_get_contents('http://...')
allow_url_include = Off ; siempre Off permite RFI (Remote File Inclusion)
php.ini global vs php_admin_value
En entornos con múltiples vhosts, puedes sobreescribir la configuración de php.ini por vhost en el fichero de configuración del servidor web:
# Apache en el VirtualHost o .htaccess
php_value memory_limit "512M" ; el script puede cambiarlo con ini_set()
php_admin_value memory_limit "512M" ; el script NO puede cambiarlo más seguro
php_flag display_errors Off
php_admin_flag display_errors Off
# Nginx + PHP-FPM en el pool de PHP-FPM (/etc/php/8.x/fpm/pool.d/www.conf)
php_admin_value[memory_limit] = 512M
php_admin_flag[display_errors] = Off
php_admin_value[error_log] = /var/log/php/miapp_error.log
Cabeceras de seguridad HTTP desde PHP
<?php
// Añadir en el bootstrap de la aplicación o en el servidor web
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}'");
header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); // solo HTTPS
// Eliminar cabeceras que revelan información
header_remove('X-Powered-By'); // aunque mejor con expose_php=Off
Configuración completa recomendada para producción
; /etc/php/8.x/fpm/php.ini producción
; Errores
display_errors = Off
display_startup_errors = Off
log_errors = On
error_reporting = E_ALL
error_log = /var/log/php/error.log
; Sesiones
session.cookie_httponly = On
session.cookie_secure = On
session.cookie_samesite = Strict
session.use_strict_mode = On
session.use_only_cookies = On
; Rendimiento
memory_limit = 256M
max_execution_time = 30
max_input_vars = 1000
realpath_cache_size = 4096k
realpath_cache_ttl = 120
; OPcache
opcache.enable = 1
opcache.memory_consumption = 128
opcache.validate_timestamps = 0
opcache.max_accelerated_files = 10000
opcache.jit_buffer_size = 64M
opcache.jit = tracing
; Seguridad
expose_php = Off
allow_url_include = Off
disable_functions = exec,passthru,shell_exec,system,popen,proc_open
Verificar la configuración activa
<?php
// Ver solo los valores que difieren de los compilados por defecto
$ini = ini_get_all();
foreach ($ini as $clave => $info) {
if ($info['global_value'] !== $info['default_value']) {
echo "$clave: {$info['global_value']}n";
}
}
// Valores específicos
echo ini_get('memory_limit'); // 256M
echo ini_get('display_errors'); //
echo ini_get('session.cookie_secure'); // 1
- Nunca copies el php.ini de desarrollo a producción sin revisar: display_errors=On, error_reporting demasiado permisivo o funciones peligrosas habilitadas son los errores más comunes.
- max_input_vars bajo puede romper formularios grandes: si tienes formularios con muchos campos (o tablas de precios complejas), ajusta este valor o verifica que los datos llegan completos.
- post_max_size debe ser mayor que upload_max_filesize: si no, la subida de ficheros parece funcionar pero los datos POST quedan truncados.
