Documento Técnico Versión: 2.0 Fecha: 15 de Diciembre de 2025 Estado: CONFIDENCIAL
- Arquitectura de Seguridad
- Password Policy
- Rate Limiting
- CSRF Protection
- Log Rotation
- Content Security Policy
- Checklist de Seguridad
shop-v2/
├── app/ # PRIVADO - Inaccesible vía HTTP
│ ├── config/ # Configuraciones sensibles
│ ├── includes/ # Funciones del sistema
│ ├── pages/ # Vistas/Controladores
│ └── data/ # JSON data files
│
└── public_html/ # PÚBLICO - Web root
├── index.php # Entry point 1
├── webhook.php # Entry point 2
├── admin/
│ ├── index.php # Entry point 3
│ └── login.php # Entry point 4
└── api/
└── index.php # Entry point 5
El sistema tiene exactamente 5 entry points:
/public_html/index.php- Frontend/public_html/webhook.php- Webhooks MercadoPago/public_html/admin/index.php- Panel admin/public_html/admin/login.php- Login admin/public_html/api/index.php- API router
Todos los demás archivos en /app/ DEBEN incluir el check de seguridad:
if (!defined('APP_ENTRY_POINT')) {
die('Direct access not permitted');
}Ubicación: app/includes/security.php
- Longitud mínima: 12 caracteres
- Longitud máxima: 128 caracteres (prevención de DoS)
- Complejidad:
- Al menos una letra mayúscula
- Al menos una letra minúscula
- Al menos un número
- Al menos un símbolo especial
Valida si una contraseña cumple con la política de seguridad.
Uso:
$password = 'MiPassword123!';
$validation = validate_password_strength($password);
if (!$validation['valid']) {
// Mostrar errores
foreach ($validation['errors'] as $error) {
echo "❌ $error\n";
}
} else {
// Contraseña válida
echo "✅ Contraseña segura (score: {$validation['strength_score']}/100)\n";
}Retorno:
[
'valid' => bool, // True si cumple todos los requisitos
'errors' => array, // Lista de errores (vacío si valid = true)
'strength_score' => int // Score de 0 a 100
]Calcula un score numérico de la fortaleza de la contraseña (0-100).
Criterios de puntuación:
- Longitud: hasta 40 puntos (2 puntos por carácter)
- Mayúsculas: 15 puntos
- Minúsculas: 15 puntos
- Números: 15 puntos
- Símbolos: 15 puntos
- Bonus complejidad: 10 puntos (si tiene >10 caracteres únicos)
Convierte el score en un nivel descriptivo.
Niveles:
| Score | Nivel | Color | Descripción |
|---|---|---|---|
| 0-29 | Muy débil | 🔴 Rojo | Fácil de adivinar |
| 30-49 | Débil | 🟠 Naranja | Podría ser más segura |
| 50-69 | Aceptable | 🟡 Amarillo | Seguridad media |
| 70-84 | Fuerte | 🟢 Verde | Buena contraseña |
| 85-100 | Muy fuerte | 🟢 Verde brillante | Excelente |
Uso:
$score = calculate_password_strength_score('MiPassword123!');
$level = get_password_strength_level($score);
echo "{$level['level']}: {$level['message']}";
// Output: "Fuerte: Buena contraseña"La validación de password policy está integrada en:
create_admin_user()- Creación de usuarioschange_admin_password()- Cambio de contraseña
Ejemplo de error:
$result = create_admin_user('admin', 'weak', 'admin@example.com');
if (!$result['success']) {
// $result['errors'] contendrá:
// [
// "La contraseña debe tener al menos 12 caracteres",
// "Debe contener al menos un símbolo especial"
// ]
}Ubicación: app/includes/rate_limit.php
Uso en endpoints:
// Permitir 10 requests por minuto
api_rate_limit(10, 60);
// Permitir 5 requests cada 10 minutos
api_rate_limit(5, 600);| Endpoint | Límite | Ventana | Propósito |
|---|---|---|---|
admin/login.php |
5 | 15 min | Prevenir brute force |
webhook.php |
100 | 1 min | Prevenir abuse |
api/validate_coupon.php |
20 | 1 min | Prevenir enumeración |
api/sync_cart.php |
30 | 1 min | Prevenir abuse |
api/cancel_order.php |
10 | 1 min | Prevenir abuse |
api/get_products.php |
30 | 1 min | Prevenir scraping |
api/get_promotion.php |
30 | 1 min | Prevenir abuse |
api/get_shared_wishlist.php |
60 | 1 min | Uso normal |
api/send-test-email.php |
5 | 10 min | Prevenir spam |
api/send-telegram-test.php |
5 | 10 min | Prevenir spam |
Los límites se almacenan en archivos JSON en app/data/rate_limits/:
app/data/rate_limits/
├── {IP}_{identifier}.json
└── ...
Cada archivo contiene:
{
"count": 3,
"first_request": 1702648200,
"last_request": 1702648230
}HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": "Rate limit exceeded",
"retry_after": 45
}
Función: generate_csrf_token()
// En el formulario
$csrf_token = generate_csrf_token();
echo '<input type="hidden" name="csrf_token" value="' . $csrf_token . '">';Función: validate_csrf_token($token)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validate_csrf_token($_POST['csrf_token'] ?? '')) {
die('CSRF token inválido');
}
// Procesar formulario
}Los tokens CSRF expiran después de 1 hora.
Para APIs, también se valida el origen de las peticiones:
$allowed_origins = [
'https://peu.net',
'http://localhost:8000',
'http://127.0.0.1:8000'
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? $_SERVER['HTTP_REFERER'] ?? '';
// Validar origin...Implementado en:
api/cancel_order.php
Ubicación: app/includes/log_rotation.php
Rota un log si excede el tamaño máximo.
Parámetros:
$log_file: Ruta completa al log$max_size_mb: Tamaño máximo en MB (default: 10)$keep_rotations: Número de rotaciones a mantener (default: 5)
Ejemplo:
// Rotar si excede 20 MB, mantener 10 rotaciones
rotate_log_if_needed('/path/to/app.log', 20, 10);Resultado:
app.log # Archivo actual
app.log.1.gz # Primera rotación (comprimida)
app.log.2.gz # Segunda rotación
...
app.log.10.gz # Décima rotación (la más antigua se elimina al rotar)
Comprime un log rotado con gzip (máxima compresión).
Elimina logs archivados más antiguos que X días (default: 90).
Rota todos los logs del sistema automáticamente.
Logs gestionados:
security.log(10 MB max, 5 rotaciones)admin_actions.log(10 MB max, 5 rotaciones)mp_logs.json(20 MB max, 10 rotaciones)webhook_log.json(20 MB max, 10 rotaciones)errors.log(50 MB max, 10 rotaciones)
Ubicación: public_html/scripts/rotate-logs.php
Ejecutar manualmente:
cd /home/pablo/shop-v2
php public_html/scripts/rotate-logs.phpSalida:
=== Rotación de Logs del Sistema ===
Fecha: 2025-12-15 14:30:00
Resultados:
- Logs rotados: 2
- Logs archivados (comprimidos): 2
- Logs antiguos eliminados: 5
✅ Rotación completada exitosamente
Ejecutar diariamente a las 3 AM:
crontab -eAgregar:
0 3 * * * cd /home/pablo/shop-v2 && php public_html/scripts/rotate-logs.php >> /tmp/log-rotation.log 2>&1Función: anonymize_ip_for_log($ip)
Las IPs en logs se hashean con SHA-256 antes de guardar:
$ip = '192.168.1.100';
$hashed = anonymize_ip_for_log($ip);
// Output: "a3f4b2c1d5e6f7a8"Función helper: secure_log($message, $level, $context)
secure_log('Usuario intentó acceso no autorizado', 'warning', [
'username' => 'admin',
'endpoint' => '/api/sensitive'
]);Resultado en log:
{
"timestamp": "2025-12-15 14:30:00",
"level": "warning",
"message": "Usuario intentó acceso no autorizado",
"ip_hash": "a3f4b2c1d5e6f7a8",
"user_agent": "Mozilla/5.0...",
"context": {
"username": "admin",
"endpoint": "/api/sensitive"
}
}Ubicación: app/includes/security.php → set_security_headers()
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{NONCE}' 'unsafe-eval' https://sdk.mercadopago.com;
style-src 'self' 'unsafe-inline';
connect-src 'self' https://api.mercadopago.com;
frame-src https://*.mercadopago.com;
Todos los scripts y estilos inline DEBEN usar nonces:
<!-- ❌ INCORRECTO - Bloqueado por CSP -->
<script>
console.log('Hola');
</script>
<!-- ✅ CORRECTO -->
<script nonce="<?= csp_nonce() ?>">
console.log('Hola');
</script>NO usar event handlers inline:
<!-- ❌ INCORRECTO -->
<button onclick="myFunction()">Click</button>
<!-- ✅ CORRECTO -->
<button data-action="myFunction">Click</button>JavaScript:
function myFunction(event, element, params) {
console.log('Clicked!');
}
// Exportar para event delegation
window.myFunction = myFunction;Incluir event-handlers.js:
<script nonce="<?= csp_nonce() ?>" src="<?= url('/assets/js/event-handlers.js') ?>"></script>- Define
APP_ENTRY_POINTal inicio - Incluye bootstrap correcto
- Implementa rate limiting con
api_rate_limit() - Valida todos los inputs con
sanitize_input() - Usa CSRF tokens para operaciones state-changing
- Valida Origin header si es crítico
- Usa
read_json()ywrite_json()para operaciones JSON - Retorna errores genéricos al usuario (no revelar detalles internos)
- Logea eventos importantes con
secure_log()
- Incluye security check:
if (!defined('APP_ENTRY_POINT')) - Usa nonces en todos los
<script>y<style>inline - NO usa event handlers inline (onclick, onchange, etc.)
- Usa
data-actioncon event delegation - Incluye
event-handlers.jsal final - Usa
url()helper para todas las URLs e imágenes - Genera y valida CSRF tokens en formularios
- Incluye modal component (
app/includes/admin/modal.php)
- Usa
hash_password()con Argon2id - Valida password policy con
validate_password_strength() - Implementa rate limiting en login (5 intentos/15 min)
- Regenera session ID después del login
- Verifica session timeout (1 hora)
- Logea intentos fallidos con
log_admin_action()
- Anonimiza IPs con
anonymize_ip_for_log() - Usa
secure_log()para eventos de seguridad - NO logees contraseñas, tokens, o datos sensibles
- Implementa rotación con
rotate_log_if_needed() - Configura cron job para rotación automática
Implementa múltiples capas de seguridad:
- Entry point validation
- Rate limiting
- CSRF protection
- Input sanitization
- Origin validation
- Session timeout
- Audit logging
- Código privado fuera de web root
- Permisos mínimos en archivos (640 para configs, 750 para directorios)
- Solo 5 entry points públicos
- Errores genéricos al usuario
- Logging detallado interno
- Rate limiting estricto en caso de fallo
- CSP estricta activada por defecto
- HTTPS enforced
- Security headers completos
- Password policy aplicada automáticamente
Para reportar vulnerabilidades o consultas de seguridad:
- Email: security@example.com
- Proceso: Responsible Disclosure
- SLA: Respuesta en 24 horas para críticos
FIN DEL DOCUMENTO
Documento confidencial - Uso interno únicamente Última actualización: 15 de Diciembre de 2025