Por que el sitio es lento — y que hacer al respecto. Why the site is slow — and what to do about it.
El servidor tarda ~3.8 segundos en generar una sola pagina. No es la red, no son las imagenes, no es el JavaScript — es el tiempo de ejecucion PHP puro en cada solicitud. WordPress esta regenerando la pagina completa desde cero para cada visitante porque no existe cache de pagina, y la combinacion de plugins (WPML + WooCommerce + Revolution Slider + Visual Composer) hace un trabajo enorme en cada peticion. The server takes ~3.8 seconds to generate a single page. That's not network, not images, not JavaScript — it's raw PHP execution time on every request. WordPress is regenerating the full page from scratch for every visitor because there is no page cache, and the plugin stack (WPML + WooCommerce + Revolution Slider + Visual Composer) is doing a lot of work on every single hit.
Tres solicitudes separadas a la pagina de inicio produjeron un TTFB de ~3.8 segundos cada una. Un cache activo reduciria esto a ~100–200 ms. El endpoint wp-json y admin-ajax.php muestran el mismo retraso de ~3.5s, lo que significa que PHP en si es lento en cada solicitud — no solo en la pagina de inicio renderizada.
Three separate requests to the homepage all produced a ~3.8 second TTFB. A warm cache would cut this to ~100–200 ms. The wp-json endpoint and admin-ajax.php both show the same ~3.5s delay, which means PHP itself is slow on every request — not just the rendered homepage.
Los encabezados de respuesta muestran Server: Apache sin huellas de CDN (cf-ray, x-cache, age ausentes). Eso significa que cada imagen, script y hoja de estilo se sirve directamente desde el servidor Apache de origen — sin cache en el borde, sin distribucion geografica, sin descarga de activos. Para un visitante en Europa o Asia, la latencia se suma al servidor ya lento.
Response headers show Server: Apache with no CDN fingerprints (no cf-ray, no x-cache, no age). That means every image, script, and stylesheet is served directly from the origin Apache box — no edge caching, no geographic distribution, no asset offload. For a visitor in Europe or Asia, latency compounds on top of the already-slow server.
El HTML carga 57 etiquetas de script externas. En HTTP/1.1, los navegadores abren solo ~6 conexiones paralelas por dominio, por lo que estos scripts se acumulan en cola en oleadas. El mismo sitio en HTTP/2 los multiplexaria todos en una sola conexion. Los mayores culpables son Revolution Slider (siempre carga su JS completo aunque no sea visible), Visual Composer, WPML y los scripts de WooCommerce que cargan incluso en paginas sin tienda. The HTML loads 57 external script tags. On HTTP/1.1, browsers open only ~6 parallel connections per domain, so these scripts queue up in waves. The same site on HTTP/2 would multiplex them all on one connection. The biggest offenders are Revolution Slider (always loads its full JS even when not visible), Visual Composer, WPML, and WooCommerce scripts loading even on non-shop pages.
El sitio ejecuta varios plugins conocidos por ser lentos y por hacer trabajo en cada solicitud sin importar si la pagina los necesita: The site is running several plugins that are well-known for being slow and doing work on every request regardless of whether the page needs them:
WPML es especialmente problematico en combinacion con WooCommerce — agrega consultas de base de datos adicionales en cada busqueda de producto. Revolution Slider y Visual Composer son constructores de paginas heredados conocidos por su exceso de codigo. WPML is especially problematic in combination with WooCommerce — it adds extra database queries on every product lookup. Revolution Slider and Visual Composer are both legacy page-builders known for bloat.
La respuesta HTML no contiene encabezados Cache-Control, Expires ni ETag, lo que significa que cada visita de regreso descarga toda la pagina de nuevo en lugar de usar una copia en cache. Esto es un sintoma de la falta de cache de pagina (ver #1) — un plugin de cache los configuraria automaticamente.
The HTML response contains no Cache-Control, Expires, or ETag headers, meaning every return visit re-downloads the entire page instead of using a cached copy. This is a symptom of the missing page cache (see #1) — a caching plugin would set these automatically.
No todo esta roto — estas partes estan bien y no necesitan atencion: Not everything is broken — these parts are fine and don't need attention:
| VerificacionCheck | ResultadoResult | EstadoStatus |
|---|---|---|
| Consulta DNSDNS lookup | 134 ms | OK |
| Conexion TCPTCP connect | 92 ms | OK |
| Handshake TLSTLS handshake | 160 ms | OK |
| Compresion GzipGzip compression | Enabled (Content-Encoding: gzip) | OK |
| Redireccion HTTPSHTTPS redirect | Direct, no redirect chain | OK |
| Minificacion de scriptsScript minification | 44 of 57 minified (77%) | MayoriaMostly |
| Tamano de imagenes (muestra)Image sizes (sampled) | ~18–32 KB per image | AceptableReasonable |
Si se hacen en este orden, el TTFB deberia bajar de ~3.8s a ~200–400 ms, y el tiempo de carga total a menos de 1.5s. Los pasos 1 y 2 por si solos eliminaran ~80% del problema. If you do these in order, TTFB should drop from ~3.8s to ~200–400 ms, and total load time to under 1.5s. Steps 1 and 2 alone will eliminate ~80% of the pain.
Reconocimiento externo unicamente — sin pruebas autenticadas, sin explotacion, sin fuerza bruta. Estos son problemas visibles para cualquier visitante o bot que escanee el sitio. External-only reconnaissance — no authenticated testing, no exploitation, no brute-force. These are issues visible to any visitor or bot scanning the site.
El servidor acepta conexiones usando TLS 1.0 y TLS 1.1, ambos obsoletos (RFC 8996, marzo 2021) y conocidos por ser vulnerables a ataques como BEAST, POODLE y Lucky13. Esto tambien es un incumplimiento de PCI DSS — PCI DSS 3.2+ requiere minimo TLS 1.2 para cualquier sitio que maneje datos de pago. Al ser una tienda WooCommerce, esto es un fallo de cumplimiento grave. The server accepts connections using TLS 1.0 and TLS 1.1, both of which are deprecated (RFC 8996, March 2021) and known to be vulnerable to attacks like BEAST, POODLE, and Lucky13. This is also a PCI DSS non-compliance issue — PCI DSS 3.2+ requires TLS 1.2 minimum for any site handling payment data. Since this is a WooCommerce store, this is a hard compliance failure.
ssl.conf de Apache, establecer SSLProtocol -all +TLSv1.2 +TLSv1.3. Si se usa un panel de hosting (cPanel/Plesk), buscar la configuracion de protocolos SSL/TLS. Cloudflare (si se agrega segun la solucion de rendimiento #2) puede aplicar esto en el borde con un interruptor: SSL/TLS → Certificados Edge → Version TLS Minima → 1.2.
In Apache's ssl.conf, set SSLProtocol -all +TLSv1.2 +TLSv1.3. If using a hosting panel (cPanel/Plesk), find the SSL/TLS protocol settings. Cloudflare (if added per performance fix #2) can enforce this at the edge with one toggle: SSL/TLS → Edge Certificates → Minimum TLS Version → 1.2.
Options +Indexes de Apache esta habilitado en /wp-content/uploads/, /wp-content/plugins/, /wp-content/themes/ y /wp-includes/. Cualquiera puede navegar cada archivo subido, cada ano de medios, directorios de cache, envios de formularios y activos de plugins. Esto es una divulgacion total de la estructura de archivos del sitio.
Apache's Options +Indexes is enabled on /wp-content/uploads/, /wp-content/plugins/, /wp-content/themes/, and /wp-includes/. Anyone can browse every uploaded file, every year's media, cache directories, form submissions, and plugin assets. This is a full information disclosure of the site's file structure.
Options -Indexes al .htaccess en la raiz de WordPress, o (mejor) configurarlo en el bloque <Directory> de Apache. Es una correccion de una sola linea.
Add Options -Indexes to .htaccess in the WordPress root, or (better) set it in Apache's <Directory> config. This is a one-line fix.
No solo estan los directorios listados — contienen archivos con datos sensibles reales. A continuacion se muestra exactamente lo que un atacante (o un bot) puede ver y descargar sin ninguna autenticacion. Todos los enlaces de abajo son en vivo. The directories aren't just listable — they contain files with real sensitive data. Below is exactly what an attacker (or a bot) can see and download with zero authentication. All links below are live.
Archivo en vivo: Live file: /wp-content/uploads/merlin-wp/main.log
ecstaticcrafts y el sitio vive en /home/ecstaticcrafts/public_html/. Si un atacante encuentra una vulnerabilidad de inclusion de archivos, ya sabe exactamente a que ruta apuntar.
The server account is named ecstaticcrafts and the site lives at /home/ecstaticcrafts/public_html/. If an attacker finds a file inclusion vulnerability, they already know exactly which path to aim at.
Fuente: el mismo log de Merlin, 30+ entradas Source: same Merlin log, 30+ entries
Fuente: Source: options-2021-06-28__13-53-10.dat — volcado serializado PHP de todas las opciones del tema — serialized PHP dump of all theme options
Fuente: Source: content-2021-06-28__13-53-10.xml
Directorio: Directory: /wp-content/uploads/useanyfont/
Directorio raiz: Root directory: /wp-content/uploads/
Directorio: Directory: /wp-content/uploads/aios/firewall-rules/
El sitio no envia ningun encabezado de seguridad. Cada respuesta carece de todos los siguientes. Esto deja el sitio completamente expuesto a clickjacking (incrustacion en iframe), ataques de deteccion de tipo MIME, reflexion de cross-site scripting y filtracion de informacion. The site sends no security headers at all. Every response is missing all of the following. This leaves the site wide open to clickjacking (iframe embedding), MIME-type sniffing attacks, cross-site scripting reflection, and information leakage.
| Header | EstadoStatus | Riesgo si faltaRisk if missing |
|---|---|---|
| Strict-Transport-Security | AusenteMissing | Los usuarios pueden ser degradados a HTTPUsers can be downgraded to HTTP |
| X-Frame-Options | AusenteMissing | Clickjacking — el sitio puede incrustarse en un iframe de un atacanteClickjacking — site can be embedded in attacker's iframe |
| X-Content-Type-Options | AusenteMissing | Ataques de deteccion de tipo MIME en uploadsMIME-type sniffing attacks on uploads |
| Content-Security-Policy | AusenteMissing | Sin mitigacion XSS, sin restriccion de scripts inlineNo XSS mitigation, no restriction on inline scripts |
| Referrer-Policy | AusenteMissing | URL de referencia completa filtrada a sitios externosFull referrer URL leaked to external sites |
| Permissions-Policy | AusenteMissing | Sin control de acceso a APIs del navegador (camara, microfono, geolocalizacion)No control over browser API access (camera, mic, geolocation) |
.htaccess:
Add to .htaccess:Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" Header always set X-Frame-Options "SAMEORIGIN" Header always set X-Content-Type-Options "nosniff" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"O usar un plugin como "HTTP Headers" o "Security Headers" si no hay acceso a
.htaccess.
Or use a plugin like "HTTP Headers" or "Security Headers" if there's no access to .htaccess.
El asistente de configuracion Merlin WP dejo un log de depuracion en /wp-content/uploads/merlin-wp/main.log que es publicamente accesible. Revela la ruta completa del sistema de archivos del servidor: /home/ecstaticcrafts/public_html/. Esta es informacion valiosa para un atacante — confirma el nombre de la cuenta de hosting y la estructura de directorios, lo que facilita ataques de path traversal e inclusion de archivos locales.
The Merlin WP setup wizard left a debug log at /wp-content/uploads/merlin-wp/main.log that's publicly accessible. It reveals the full server filesystem path: /home/ecstaticcrafts/public_html/. This is valuable intelligence for an attacker — it confirms the hosting account name and directory structure, which aids in path traversal and local file inclusion attacks.
/wp-content/uploads/merlin-wp/ completo. El asistente de configuracion del tema solo se usa una vez durante la instalacion inicial.
Delete the entire /wp-content/uploads/merlin-wp/ directory. The theme setup wizard is only used once during initial installation.
All-In-One Security (AIOS) esta instalado, pero su directorio de reglas de firewall en /wp-content/uploads/aios/firewall-rules/ es publicamente navegable. Esto expone settings.php y potencialmente revela que reglas de firewall estan activas — dando a un atacante un mapa de que protecciones existen y como evitarlas.
All-In-One Security (AIOS) is installed, but its firewall rules directory at /wp-content/uploads/aios/firewall-rules/ is publicly browseable. This exposes settings.php and potentially reveals which firewall rules are active — giving an attacker a map of what protections exist and how to bypass them.
Multiples plugins exponen sus numeros de version exactos a traves de archivos readme.txt y parametros de cadena de consulta en el codigo fuente HTML. Un atacante puede cruzar estos datos con bases de datos publicas de CVE (WPScan, NVD) para encontrar exploits conocidos que funcionen con estas versiones exactas.
Multiple plugins expose their exact version numbers through readme.txt files and query string parameters in the HTML source. An attacker can cross-reference these against public CVE databases (WPScan, NVD) to find known exploits that work on these exact versions.
define('SCRIPT_DEBUG', false) y usar un plugin como "Remove WP Version" para eliminar las etiquetas generator). (d) Deshabilitar el listado de directorios (S2) para evitar la navegacion de readme.txt en los directorios de plugins.
(a) Keep all plugins and WP core up to date — the best defense against CVE-based attacks. (b) Remove readme.txt and readme.html from the WP root. (c) Remove version query strings from enqueued assets (add define('SCRIPT_DEBUG', false) and use a plugin like "Remove WP Version" to strip generator tags). (d) Disable directory listing (S2) to prevent readme.txt browsing in plugin dirs.
wp-cron.php devuelve 200 y activa el sistema cron de WordPress. Un atacante puede enviar miles de solicitudes a este endpoint para sobrecargar el servidor con tareas programadas. En un servidor que ya tarda ~3.8s en renderizar una pagina, esto provocaria un agotamiento rapido de recursos.
wp-cron.php returns 200 and triggers WordPress's cron system. An attacker can send thousands of requests to this endpoint to overload the server with scheduled tasks. On a server that already takes ~3.8s to render a page, this would cause rapid resource exhaustion.
wp-config.php: define('DISABLE_WP_CRON', true); y configurar un cron real del sistema en el servidor (*/15 * * * * curl -s https://ecstaticcrafts.com/wp-cron.php). Esto tambien mejora el rendimiento ya que WordPress no verificara el cron en cada carga de pagina.
Disable WP-Cron via wp-config.php: define('DISABLE_WP_CRON', true); and set up a real system cron on the server (*/15 * * * * curl -s https://ecstaticcrafts.com/wp-cron.php). This also improves performance since WordPress won't check cron on every page load.
La pagina de instalacion de WordPress es publicamente accesible y confirma "You appear to have already installed WordPress" junto con un enlace directo a la pagina de inicio de sesion. Si bien no es directamente explotable en su estado actual, le confirma a un atacante que esto es una instalacion de WordPress y proporciona un punto de apoyo para mayor reconocimiento. The WordPress installation page is publicly accessible and confirms "You appear to have already installed WordPress" along with a direct link to the login page. While not directly exploitable in its current state, it confirms to an attacker that this is a WordPress installation and provides a fingerhold for further reconnaissance.
install.php via .htaccess:
Block access to install.php via .htaccess:<Files install.php>
Order allow,deny
Deny from all
</Files>
Solicitar /?author=1 devuelve una redireccion 301 a la pagina de inicio (confirmando que el usuario ID 1 existe), mientras que /?author=2 devuelve 404. Esto permite a un atacante enumerar IDs de usuario validos y, combinado con vulnerabilidades de plugins, intentar ataques de relleno de credenciales o fuerza bruta en cuentas confirmadas.
Requesting /?author=1 returns a 301 redirect to the homepage (confirming user ID 1 exists), while /?author=2 returns 404. This allows an attacker to enumerate valid user IDs and, when combined with plugin vulnerabilities, attempt credential stuffing or brute-force attacks on confirmed accounts.
?author=1 a la URL, cualquiera puede verificar si un usuario con ese ID existe. 301 = existe, 404 = no. Permite filtrar usuarios reales antes de intentar adivinar contrasenas — como revisar que apartamentos estan ocupados antes de forzar cerraduras.
Adding ?author=1 to the URL lets anyone check if a user with that ID exists. 301 = exists, 404 = no. It lets an attacker filter out real accounts before guessing passwords — like checking which apartments are occupied before picking locks.
.htaccess:
AIOS (already installed) has a setting to disable author archives. Enable it: AIOS → Brute Force → Login Lockout. Alternatively, add to .htaccess:RewriteRule ^/?author=([0-9]*) / [R=301,L]
No todo esta mal — esto esta correctamente configurado: Not everything is bad — these are configured correctly:
| VerificacionCheck | ResultadoResult | EstadoStatus |
|---|---|---|
| API WooCommerce (productos, pedidos, clientes)WooCommerce API (products, orders, customers) | 403 Forbidden | OK |
| API REST de WPMLWPML REST API | 403 Forbidden | OK |
| wp-login.php | 404 (hidden/moved) | OK |
| Metodo TRACETRACE method | 405 Method Not Allowed | OK |
| Redireccion HTTP → HTTPSHTTP → HTTPS redirect | 301 redirect, X-Redirect-By: WordPress | OK |
| Redireccion www → sin wwwwww → non-www redirect | 301 redirect | OK |
| .git/config | 404 | OK |
| .env | 301 → homepage (rewrite, not exposed) | OK |
| wp-config.php | 403 Forbidden | OK |
| Copias de seguridad de wp-config.phpwp-config.php backups | All 403 | OK |
| phpinfo.php / info.php | 404 | OK |
| Certificado TLSTLS certificate | Let's Encrypt R13, valid until 2026-07-05, wildcard *.ecstaticcrafts.com | OK |
| debug.log | 404 | OK |
| Endpoint de usuarios via REST APIREST API user endpoint | Empty/blocked | OK |
SSLProtocol -all +TLSv1.2 +TLSv1.3 en la configuracion de Apache. Obligatorio para el cumplimiento de PCI DSS en cualquier tienda WooCommerce que procese pagos. Si se usa Cloudflare (de la solucion de rendimiento #2), es un solo interruptor.
Set SSLProtocol -all +TLSv1.2 +TLSv1.3 in Apache config. Mandatory for PCI DSS compliance on any WooCommerce store handling payments. If using Cloudflare (from performance fix #2), this is a single toggle.
Options -Indexes al .htaccess raiz. Corrige S2, S5 (exposicion de AIOS) y bloquea la navegacion de archivos de plugins. Una correccion de una linea que cierra la fuga de informacion mas peligrosa.
Add Options -Indexes to the root .htaccess. Fixes S2, S5 (AIOS exposure), and blocks plugin file browsing. One-line fix that closes the most dangerous information leak.
.htaccess. El CSP puede agregarse despues (requiere pruebas cuidadosas con 57 scripts + dominios externos).
Add HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy to .htaccess. CSP can be added later (needs careful testing with 57 scripts + external domains).
/wp-content/uploads/merlin-wp/ completamente. Contiene la fuga de ruta del servidor y nunca se necesita despues de la configuracion inicial del tema.
Remove /wp-content/uploads/merlin-wp/ entirely. It contains the server path leak and is never needed after initial theme setup.
readme.html y license.txt de la raiz de WP. Quitar la etiqueta meta generator. Eliminar versiones en cadenas de consulta de JS/CSS. Esto dificulta los ataques CVE dirigidos (seguridad por oscuridad, pero igual tiene valor).
Remove readme.html and license.txt from the WP root. Remove the generator meta tag. Strip query string versions from JS/CSS. This makes targeted CVE attacks harder (security through obscurity, but still valuable).
define('DISABLE_WP_CRON', true); a wp-config.php y configurar una tarea cron del lado del servidor. Previene la amplificacion DDoS y mejora ligeramente el rendimiento de carga de paginas.
Add define('DISABLE_WP_CRON', true); to wp-config.php and configure a server-side cron job. Prevents DDoS amplification and slightly improves page load performance.
Todas las versiones detectadas externamente desde respuestas publicas. Mantenerlas actualizadas es la practica de seguridad mas importante. All versions detected externally from public responses. Keeping these up to date is the single most important security practice.
| SoftwareSoftware | VersionVersion | FuenteSource |
|---|---|---|
| WordPress | 6.9.4 | Meta generator HTMLHTML meta generator |
| WooCommerce | 10.5.3 (meta) / 10.5.2 (readme) | Meta HTML + readme.txtHTML meta + readme.txt |
| WPML | 4.9.0 | Meta HTML + readme.txtHTML meta + readme.txt |
| Revolution Slider | 6.7.41 | Cadena de consulta en activo JSJS asset query string |
| Yoast SEO | 27.0 | Comentario HTMLHTML comment |
| Max Mega Menu | 3.6.2 | readme.txt |
| Goya theme | detected (child theme) | Codigo fuente HTMLHTML source |
| Apache | version hidden | Encabezado ServerServer header |
| Certificado TLSTLS certificate | Let's Encrypt R13, expires 2026-07-05 | OpenSSL |
Sesion de optimizacion en el panel de WordPress. Estado antes/despues de cada hallazgo: Optimization session in the WordPress admin panel. Before/after state of each finding:
| AccionAction | DetalleDetail | EstadoStatus |
|---|---|---|
| WP Rocket — cache de paginaWP Rocket — page cache | Activado + optimizado. Cache confirmado via firma HTML.Activated + optimized. Cache confirmed via HTML signature. | HechoDone |
| WP Rocket — JS diferidoWP Rocket — deferred JS | Load JS Deferred + Delay JS Execution activadosLoad JS Deferred + Delay JS Execution enabled | HechoDone |
| WP Rocket — CSS optimizadoWP Rocket — CSS optimization | Minify CSS + Remove Unused CSS activadosMinify CSS + Remove Unused CSS enabled | HechoDone |
| WP Rocket — LazyLoadWP Rocket — LazyLoad | Imagenes, iframes, CSS backgroundsImages, iframes, CSS backgrounds | HechoDone |
| WP Rocket — HeartbeatWP Rocket — Heartbeat | Backend + frontend deshabilitados, editor reducidoBackend + frontend disabled, editor reduced | HechoDone |
| WP Rocket — Base de datosWP Rocket — Database | 1,972 transients limpiados. Limpieza diaria automatica.1,972 transients cleaned. Daily auto-cleanup enabled. | HechoDone |
| WP Rocket — PrecargaWP Rocket — Preload | Precarga de paginas + precarga de enlaces activadasPage preloading + link preloading enabled | HechoDone |
| Limpieza de pluginsPlugin cleanup | ~24 inactivos eliminados + 12 activos desactivados. De ~55 a ~25 plugins activos.~24 inactive deleted + 12 active deactivated. From ~55 to ~25 active plugins. | HechoDone |
| Cache de /tienda/ reparado/tienda/ cache fixed | Eliminada de la lista "Never Cache" — la pagina de tienda ahora se sirve desde cacheRemoved from "Never Cache" list — shop page now served from cache | HechoDone |
| Asset CleanUp — instalado— installed | Plugin instalado y configurado. Detecta WP Rocket y evita duplicar minificacion.Plugin installed and configured. Detects WP Rocket and avoids duplicating minification. | HechoDone |
| Asset CleanUp — Limpieza HTML— HTML cleanup | Eliminados: emoji scripts, etiqueta meta generator (esconde version WP), oEmbed, RSD, Windows Live Writer, wlwmanifest, REST API linksRemoved: emoji scripts, generator meta tag (hides WP version), oEmbed, RSD, Windows Live Writer, wlwmanifest, REST API links | HechoDone |
| Asset CleanUp — XML-RPC— XML-RPC | XML-RPC deshabilitado — vector comun de fuerza bruta cerradoXML-RPC disabled — common brute force attack vector closed | HechoDone |
| Cloudflare CDN | Requiere cambio de nameservers — bloqueado por ahoraRequires nameserver change — blocked for now | PendientePending |
| Asset CleanUp — Descarga selectiva por pagina— Per-page script unloading | No realizado aun — descargar scripts de WooCommerce/FormCraft/Wishlist en homepage y paginas no-tiendaNot done yet — unload WooCommerce/FormCraft/Wishlist scripts on homepage and non-shop pages | En progresoIn progress |
| HallazgoFinding | AntesBefore | DespuesAfter | EstadoStatus |
|---|---|---|---|
| S1. TLS 1.0 | HabilitadoEnabled | BloqueadoBlocked | ResueltoSolved |
| S1. TLS 1.1 | HabilitadoEnabled | Bloqueado (verificado Fase 3)Blocked (verified Phase 3) | ResueltoSolved |
| S2. Listado de directoriosS2. Directory listing | HTTP 200 — navegablebrowseable | HTTP 404 — bloqueadoblocked | ResueltoSolved |
| S3. Encabezados de seguridadS3. Security headers | NingunoNone | Funcionan en subdominios, no en dominio principal (ver S18)Work on subdomains, not on main domain (see S18) | ParcialPartial |
| S4. Log de Merlin (ruta del servidor)S4. Merlin log (server path) | HTTP 200 | HTTP 200 — directorio bloqueado pero archivo aun accesibledirectory blocked but file still accessible | ParcialPartial |
| S5. Dir de reglas AIOSS5. AIOS rules dir | HTTP 200 — navegablebrowseable | HTTP 404 | ResueltoSolved |
| S6. Versiones de plugins expuestasS6. Plugin versions exposed | readme.html, license.txt, generator meta | 404 + meta generator eliminadaremoved | ResueltoSolved |
| S7. wp-cron.php | HTTP 200 | Aun accesible — necesita acceso a wp-config.phpStill accessible — needs wp-config.php access | PendientePending |
| S8. install.php | HTTP 200 | HTTP 404 | ResueltoSolved |
| S9. Enumeracion de autoresS9. Author enumeration | HTTP 301 — usuario confirmadouser confirmed | HTTP 301 — redirige pero aun detecta el usuarioredirects but still detects user | ParcialPartial |
| Archivos por defecto de WPWP default files | readme.html, license.txt accesiblesaccessible | Eliminados + auto-eliminar tras actualizacionesDeleted + auto-delete on updates | ResueltoSolved |
| Edicion de archivos PHPPHP file editing | HabilitadoEnabled | Deshabilitado via AIOSDisabled via AIOS | ResueltoSolved |
| Hotlinking | PermitidoAllowed | Bloqueado via AIOSBlocked via AIOS | ResueltoSolved |
Sesion de acceso directo al servidor. Se establecio SSH + WP-CLI + MCP para control completo del stack, seguido de una pasada de recon profundo y nueve mejoras rapidas (Q1-Q9) sin impacto al cliente. Direct server-access session. Established SSH + WP-CLI + MCP for full stack control, followed by a deep recon pass and nine quick wins (Q1-Q9) with no client-facing impact.
| CapaLayer | DetalleDetail | EstadoStatus |
|---|---|---|
| SSH | Llave ed25519 autorizada en cPanel. Acceso directo como usuario ecstaticcrafts@cp.ecstaticcrafts.com via puerto 22.ed25519 key authorized in cPanel. Direct access as ecstaticcrafts@cp.ecstaticcrafts.com via port 22. | ActivoActive |
| WP-CLI | v2.12 instalado en ~/bin/wp. Control completo de WP, WC, DB, opciones, usuarios, cron, plugins sin pasar por la UI.v2.12 installed at ~/bin/wp. Full control of WP, WC, DB, options, users, cron, plugins without going through the UI. | ActivoActive |
| MCP (WordPress) | mcp-wordpress v3.1.21 configurado. Usuario ormus-ops (admin) + Application Password. 59 herramientas REST expuestas nativamente al asistente.mcp-wordpress v3.1.21 configured. ormus-ops user (admin) + Application Password. 59 REST tools exposed natively to the assistant. | Activo (requiere reinicio)Active (restart required) |
| HallazgoFinding | AntesBefore | DespuesAfter | EstadoStatus |
|---|---|---|---|
| Permisos de wp-config.phpwp-config.php permissions | 644 — DB pass legible por otros usuarios del SODB pass readable by other OS users | 600 — solo el propietarioowner only | ResueltoSolved |
| WP_ALLOW_REPAIR anonimoAnonymous WP_ALLOW_REPAIR | /wp-admin/maint/repair.php habilitado y ejecutable sin autenticacionenabled and executable without authentication | Constante removida — pagina solo muestra instruccionConstant removed — page only shows instruction | ResueltoSolved |
| Ejecucion PHP en /uploads/PHP execution in /uploads/ | Permitido — ej. aios/firewall-rules/settings.php devuelve 200Allowed — e.g. aios/firewall-rules/settings.php returns 200 | Bloqueado via .htaccess FilesMatch (php|phtml|phar) — verificado 404Blocked via .htaccess FilesMatch (php|phtml|phar) — verified 404 | ResueltoSolved |
| Email de admin de WordPressWP admin email | web@zuliatec.com (dev original, tercero)(original dev, third party) | Sin cambio — requiere decision del clienteUnchanged — client decision required | PendientePending |
| Cuentas admin de proveedoresVendor admin accounts | web2021, supportwpindeed, wpmlsupport — 3 admins externos activos— 3 external admins active | Documentados, sin cambio — requiere decision del clienteDocumented, unchanged — client decision required | PendientePending |
| AIOS bloqueo REST anonimoAIOS anonymous REST block | Activo — bloqueaba enumeracion /wp-json/usersActive — blocked /wp-json/users enumeration | Desactivado (necesario para MCP). Mitigacion: IP allowlist + rate limiting pendienteDisabled (required for MCP). Mitigation: IP allowlist + rate limiting pending | Compromiso conscienteConscious tradeoff |
| AIOS bloqueo Application PasswordsAIOS Application Passwords block | ActivoActive | Desactivado para permitir auth MCPDisabled to allow MCP auth | Compromiso conscienteConscious tradeoff |
| AccionAction | DetalleDetail | ImpactoImpact | EstadoStatus |
|---|---|---|---|
| Mu-plugin muerto eliminadoDead mu-plugin removed | p3-profiler.php (stub de 94 bytes lanzando fatales desde 2024)(94-byte stub throwing fatals since 2024) | Menos errores PHP por requestFewer PHP errors per request | HechoDone |
| Opciones autoload basuraStale autoload options | p3_scan_2021 (58 KB), GTranslate (39 KB), evo_data_log (31 KB) — todas cargadas en cada requestp3_scan_2021 (58 KB), GTranslate (39 KB), evo_data_log (31 KB) — all loaded on every request | -128 KB por pagina (715 KB → 587 KB)-128 KB per page (715 KB → 587 KB) | HechoDone |
| Log de errores truncadoError log truncated | 15 MB → 0. Ultimas 2000 lineas guardadas en ~/_ormus_ops/logs/.15 MB → 0. Last 2000 lines saved to ~/_ormus_ops/logs/. | 15 MB de disco liberados15 MB disk freed | HechoDone |
| ZIPs obsoletos en /plugins/Stale .zip files in /plugins/ | 3 archivos (codecanyon-ump, 2x easypost) movidos a archivo3 files (codecanyon-ump, 2x easypost) moved to archive | 17 MB movidos de webroot17 MB moved out of webroot | HechoDone |
| Backup de 1.5 GB en $HOME1.5 GB backup in $HOME | backup_EC_18022026.zip movido fuera de $HOME raiz a ~/_ormus_ops/archives/backup_EC_18022026.zip moved out of $HOME root to ~/_ormus_ops/archives/ | Archivado, pendiente eliminar tras backup frescoArchived, pending delete after fresh backup | HechoDone |
| Limites PHP (admin uploads)PHP limits (admin uploads) | .user.ini: upload 2M→32M, post 8M→64M, memoria 512M, ejecucion 300s.user.ini: upload 2M→32M, post 8M→64M, memory 512M, execution 300s | Admin puede subir imagenes modernasAdmin can upload modern-size images | Hecho (FPM TTL 5 min)Done (FPM TTL 5 min) |
| Transients expiradosExpired transients | 6 filas eliminadas. Cron diario ya programado (delete_expired_transients).6 rows deleted. Daily cron already scheduled (delete_expired_transients). | Mantenimiento continuoOngoing maintenance | HechoDone |
| Compresion gzip restauradaGzip compression restored | WP Rocket descomprimia antes de servir. Solucion: zlib.output_compression=On en .user.ini. Homepage: 232 KB → 44.5 KB (-81%). EN: 387 KB → 54 KB (-86%). Cold: 142 KB → 30 KB (-79%).WP Rocket was decompressing before serving. Fix: zlib.output_compression=On in .user.ini. Homepage: 232 KB → 44.5 KB (-81%). EN: 387 KB → 54 KB (-86%). Cold: 142 KB → 30 KB (-79%). | Peso de pagina 5x menor5x smaller page weight | HechoDone |
WooCommerce sigue usando la tabla wp_posts para ordenes (cklvz_posts). El nuevo almacenamiento de alto rendimiento (HPOS / cklvz_wc_orders) no esta activo. Migrar reduce tiempo de carga del panel de ordenes significativamente, pero requiere verificar compatibilidad con el plugin "WooCommerce Legacy REST API" instalado.WooCommerce still uses wp_posts for orders (cklvz_posts). The new High-Performance Order Storage (HPOS / cklvz_wc_orders) is not active. Migrating significantly reduces admin order page load times, but requires verifying compatibility with the installed "WooCommerce Legacy REST API" plugin.
Estado on-hold tipicamente significa ordenes esperando confirmacion de transferencia bancaria. La mayoria probablemente son ordenes abandonadas. Limpiar con wc order cancel --status=on-hold --before="180 days ago" reduciria cklvz_posts/cklvz_postmeta significativamente. Requiere decision del cliente sobre ciclo de vida de datos de clientes.On-hold status typically means orders awaiting bank transfer confirmation. Most are likely abandoned. Cleaning via wc order cancel --status=on-hold --before="180 days ago" would shrink cklvz_posts/cklvz_postmeta significantly. Requires client decision on customer data lifecycle.
De las ultimas 2000 lineas del log de errores, 771 corresponden a sitepress-multilingual-cms. Patron: cuando la conexion DB hace hipo, SitePress::get_current_language() recibe null y el filtro locale explota con Call to a member function get_requested_lang() on null. Cosmetico en condiciones normales pero oscurece bugs reales. Es el mayor contribuyente a ruido en logs. Ademas WPML + plugins hermanos ocupan ~152 MB de disco en plugins/.Of the last 2000 lines of the error log, 771 come from sitepress-multilingual-cms. Pattern: when DB connection hiccups, SitePress::get_current_language() receives null and the locale filter explodes with Call to a member function get_requested_lang() on null. Cosmetic under normal conditions but obscures real bugs. The single biggest contributor to log noise. Additionally, WPML + sibling plugins consume ~152 MB of disk in plugins/.
Las carpetas ~/public_html/app.ecstaticcrafts.com/ y ~/public_html/api.ecstaticcrafts.com/ son directorios placeholder de 8 KB (verificacion Google + index.html). La aplicacion React de registro de arte y el backend Django viven en un servidor separado. Los hallazgos S11 y S21-S26 (source maps expuestos, DEBUG=True en Django, etc.) no pueden remediarse desde este cPanel — requieren acceso al servidor que hospeda realmente el subdominio app/api.The folders ~/public_html/app.ecstaticcrafts.com/ and ~/public_html/api.ecstaticcrafts.com/ are 8 KB placeholder directories (Google verification + index.html). The React art ownership registry app and Django backend live on a separate server. Findings S11 and S21-S26 (exposed source maps, DEBUG=True in Django, etc.) cannot be remediated from this cPanel — they require access to the actual hosting server for the app/api subdomains.
| AccionAction | Por que esperaWhy it waits |
|---|---|
| PHP 7.4 → 8.2PHP 7.4 → 8.2 | Mejora 30-50% en velocidad de generacion. Requiere clonar a staging y probar revslider, js_composer, plugins de CodeCanyon antes de cambiar produccion.30-50% speed improvement in page generation. Requires staging clone and testing of revslider, js_composer, CodeCanyon plugins before flipping production. |
| Cloudflare | TTFB global <200ms + HTTP/2/3 + Brotli. Requiere acceso a nameservers DNS + sign-off del cliente.Global TTFB <200ms + HTTP/2/3 + Brotli. Requires DNS nameserver access + client sign-off. |
| Migracion WC HPOSWC HPOS migration | Verificar primero que WC Legacy REST API no esta en uso por integraciones externas.First verify WC Legacy REST API is not in use by external integrations. |
| Limpieza de admins de proveedoresVendor admin cleanup | Eliminar web2021 / supportwpindeed / wpmlsupport tras confirmar con Diego que no estan troubleshooting activo.Remove web2021 / supportwpindeed / wpmlsupport after confirming with Diego none are actively troubleshooting. |
| 9 plugins inactivos a eliminar9 inactive plugins to remove | ~50 MB ahorrados. Confirmar con Diego que ninguno es "apagado pero necesario".~50 MB saved. Confirm with Diego that none is "off but needed". |
| Limpieza wc-on-hold historicosHistoric wc-on-hold cleanup | Decision del cliente sobre retencion de datos. ~800 ordenes >180 dias candidatas.Client decision on data retention. ~800 orders >180 days candidates. |
| Cambio admin_email | Cambiar de web@zuliatec.com a email controlado por el cliente. Diego debe elegir destino.Change from web@zuliatec.com to a client-controlled email. Diego must pick destination. |
| Optimizacion WebP masivaBulk WebP optimization | 1.3 GB en uploads/ incluyendo 9 fotos de telefono de 5-7 MB en formcraft3. Requiere backup antes y prueba en subset.1.3 GB in uploads/ including 9 phone photos of 5-7 MB in formcraft3. Requires backup first and test on subset. |
Pasada SSH-only enfocada exclusivamente en velocidad. Limpieza profunda de tablas huerfanas de plugins inactivos y optimizacion de autoload + DB. Se descubrio que OPcache no esta disponible en este hosting (el proveedor lo excluyo de los paquetes EA-PHP). SSH-only pass focused exclusively on speed. Deep cleanup of orphan tables from inactive plugins and autoload + DB optimization. Discovered OPcache is not available on this hosting (the provider excluded it from EA-PHP packages).
| TablaTable | Plugin origenSource plugin | AntesBefore | DespuesAfter | EstadoStatus |
|---|---|---|---|---|
| cklvz_wpml_mails | wp-mail-logging (inactivo)(inactive) | 362 MB / 14,114 filasrows | 0 / 0 | TruncadoTruncated |
| cklvz_wsal_metadata | wp-activity-log-for-woocommerce (inactivo)(inactive) | 65 MB | 0 | TruncadoTruncated |
| cklvz_wsal_occurrences | wp-activity-log-for-woocommerce (inactivo)(inactive) | 28 MB | 0 | TruncadoTruncated |
| cklvz_actionscheduler_actions | woocommerce | 1,592 completadascomplete | 0 | Limpiado (retencion)Cleaned (retention) |
| OpcionOption | TamanoSize | AccionAction |
|---|---|---|
| _transient_wp_core_block_css_files | 22 KB | Eliminado (transient que nunca debio autoload)Deleted (transient that should never autoload) |
| revslider-addons | 35 KB | autoload=no (leido solo en pags del plugin)autoload=no (read only on plugin pages) |
| ihc_levels, ihc_user_fields | 47 KB | autoload=no (indeed-membership-pro)(indeed-membership-pro) |
| evcal_options_evcal_1/2/3, evcal_styles | 70 KB | autoload=no (eventON, cargado solo en pags de calendario)(eventON, loaded only on calendar pages) |
| woocommerce_wf_easypost_id_* | 33 KB | autoload=no (easypost shipping)(easypost shipping) |
| AccionAction | DetalleDetail | EstadoStatus |
|---|---|---|
| DISABLE_WP_CRON | Habilitado en wp-config.php. Quita el "impuesto cron" de requests de usuarios (jitter 100-500ms).Enabled in wp-config.php. Removes cron tax from user requests (100-500ms jitter). | HechoDone |
| Cron real via cPanelReal cron via cPanel | Pendiente: Ormus debe agregar "cada 5 min: wp cron event run --due-now"Pending: Ormus must add "every 5 min: wp cron event run --due-now" | Pendiente (accion manual)Pending (manual action) |
| WPML null-guard mu-plugin | Suprime los 771 fatales de WPML durante estados dead_db. Cosmetico — solo deja de ensuciar logs.Suppresses the 771 WPML fatals during dead_db states. Cosmetic — just stops log pollution. | HechoDone |
Probado: el modulo opcache.so no esta instalado para NINGUNA version de PHP disponible en este hosting (ea-php73, 74, 80, 81, 82, 83). get_loaded_extensions() no incluye "Zend OPcache". opcache_get_status() es undefined. APCu tampoco esta disponible. El proveedor de hosting excluyo estos modulos de sus paquetes EA-PHP — esto es inusual (la mayoria de hostings cPanel traen OPcache por defecto).Probed: the opcache.so module is not installed for ANY PHP version available on this hosting (ea-php73, 74, 80, 81, 82, 83). get_loaded_extensions() does not include "Zend OPcache". opcache_get_status() is undefined. APCu is also unavailable. The hosting provider excluded these modules from their EA-PHP packages — this is unusual (most cPanel hosts ship OPcache by default).
ImpactoImpact: cada request PHP re-parsea miles de archivos desde disco. Es la unica palanca dominante restante para TTFB frio. Sin OPcache, el piso se queda en ~3.7s.every PHP request re-parses thousands of files from disk. This is the only remaining dominant lever for cold TTFB. Without OPcache, the floor stays at ~3.7s.
AccionAction: abrir ticket al hosting solicitando instalar `ea-php82-php-opcache` (paquete oficial de cPanel). Con OPcache activado se espera reducir el TTFB frio a ~1.5-2s (~−50%). Borrador de ticket listo.open hosting ticket requesting installation of `ea-php82-php-opcache` (official cPanel package). With OPcache active we expect cold TTFB to drop to ~1.5-2s (~−50%). Ticket draft ready.
La probe inicial via SSH mostraba PHP 7.4.33, pero era la version CLI por defecto del shell. El SAPI web (cgi-fcgi) realmente corre ea-php82 (PHP 8.2.30). Esto significa que el "upgrade a PHP 8" ya estaba hecho — el sitio se beneficia de los ~30% de mejora vs 7.4 que trae PHP 8.x a nivel de motor. Este descubrimiento re-enmarca el techo: los 3.7s frios son con PHP 8.2 sin OPcache, no con 7.4.The initial SSH probe showed PHP 7.4.33, but that was the shell's default CLI version. The web SAPI (cgi-fcgi) actually runs ea-php82 (PHP 8.2.30). This means the "PHP 8 upgrade" was already done — the site already benefits from the ~30% engine-level improvement over 7.4. This reframes the ceiling: the 3.7s cold is with PHP 8.2 without OPcache, not with 7.4.
Todas las optimizaciones de software posibles estan aplicadas. El TTFB bajo de 3.82s a ~1.2s. El piso de 1.2s es el servidor mismo — incluso sirviendo HTML estatico desde cache, tarda mas de un segundo en entregarlo. No hay configuracion de WordPress que arregle eso. La siguiente mejora requiere Cloudflare o mejor hosting. All possible software optimizations are applied. TTFB dropped from 3.82s to ~1.2s. The 1.2s floor is the server itself — even serving static HTML from cache, it takes over a second to deliver it. No WordPress configuration fixes that. The next improvement requires Cloudflare or better hosting.
Seis pruebas independientes. Todas llegan a la misma conclusion: el sitio vive en un servidor cPanel compartido de una agencia pequena (Zuliatec), en un data center rentado (Telx, Nueva York). Six independent tests. All arrive at the same conclusion: the site lives on a shared cPanel server from a small agency (Zuliatec), in a rented data center (Telx, New York).
/cpanel y /webmail responden con paginas oficiales de cPanel. Solo existen en servidores con cPanel instalado — el panel de control tipico de hosting compartido. Un VPS propio o un servidor dedicado no las tiene auto-creadas.
The /cpanel and /webmail paths return official cPanel pages. They only exist on servers with cPanel installed — the typical shared hosting control panel. A self-managed VPS or dedicated server doesn't auto-create them.
Fuente: codigo HTML de ecstaticcrafts.com contiene referencias a Source: HTML source of ecstaticcrafts.com contains references to zuliatec.com
Fuente: escaneo HTTP del bloque /24 67.23.63.0/24 + consulta a Shodan InternetDB para cada IP activa. Los resultados revelan un vecindario heterogeneo de servidores individuales colocados en el mismo rack Telx — algunos con problemas graves de parches.
Source: HTTP scan of the /24 block 67.23.63.0/24 + Shodan InternetDB query for each active IP. The results reveal a heterogeneous neighborhood of individual servers colocated in the same Telx rack — some with severe patching problems.
| IP | Host | Stack | CVEs |
|---|---|---|---|
| .58 | (no hostname) | Microsoft IIS 10 + ASP.NET + jQuery 1.7.2 | 6 |
| .60 | nerdssupport.com | Microsoft IIS 10 + Windows | 0 |
| .132 | (no hostname) | Apache 2.4.58 / Ubuntu | 37 |
| .133 | esriven.com + cp.esriven.com | cPanel · Apache · Exim 4.96 · OpenSSH 7.4 | 27 |
| .135 | tagscredit.com + kibovision.dinoiadevs.com.ar | Nginx 1.14.2 + Directus CMS | 9 |
| .136 | ecstaticcrafts.com + cp.ecstaticcrafts.com | cPanel · Apache · Exim 4.95 · OpenSSH 7.4 · MySQL · Pure-FTPd | 32 |
| .167 | (no hostname) | OpenSSH 7.4 + VNC (5900) + SNMP — likely mgmt interface | 21 |
| .171 | (no hostname) | Apache 2.2.22 / Debian · PHP 5.4.45 · OpenSSH 6.0p1 (todos EOLall EOL) | 180+ |
Shodan InternetDB reporta 32 vulnerabilidades publicas conocidas afectando el software que corre debajo de WordPress en ecstaticcrafts.com: Shodan InternetDB reports 32 known public vulnerabilities affecting the software running beneath WordPress on ecstaticcrafts.com:
| ProblemaProblem | Impacto en Ecstatic CraftsImpact on Ecstatic Crafts |
|---|---|
| Recursos compartidosShared resources | Cuando otro sitio en el mismo servidor recibe trafico, el rendimiento de ecstaticcrafts.com baja automaticamente. Observado: TTFB variando de 1.17s a 2.93s hoy (vs 1.2s ayer).When another site on the same server gets traffic, ecstaticcrafts.com performance automatically drops. Observed: TTFB varying from 1.17s to 2.93s today (vs 1.2s yesterday). |
| Sin redundancia DNSNo DNS redundancy | Un solo punto de fallo. Si el servidor cae, DNS cae, email cae, sitio cae — todo simultaneamente. Sin failover.Single point of failure. If the server goes down, DNS goes down, email goes down, site goes down — all simultaneously. No failover. |
| Conflicto de interesConflict of interest | El desarrollador (Zuliatec) es tambien el proveedor de hosting. Cualquier queja sobre velocidad va al mismo lugar que decidio cuanto hardware asignar.The developer (Zuliatec) is also the hosting provider. Any speed complaint goes to the same party that decided how much hardware to allocate. |
| Sin control de la infraestructuraNo infrastructure control | Para habilitar mod_headers o deshabilitar TLS 1.1 — hay que pedirle al proveedor. Esto explica por que los encabezados de seguridad S3 no aparecen aunque el .htaccess esta correcto: el modulo necesario no esta habilitado en el servidor.To enable mod_headers or disable TLS 1.1 — you have to ask the provider. This explains why security headers from S3 don't appear despite correct .htaccess rules: the required module is not enabled on the server. |
Cloudflare actua como un intermediario entre los visitantes y el servidor. Copia las paginas, imagenes, CSS y JavaScript a mas de 300 servidores alrededor del mundo. Cuando alguien visita el sitio, Cloudflare le entrega la copia mas cercana en vez de ir hasta el servidor original. Cloudflare acts as an intermediary between visitors and the server. It copies pages, images, CSS, and JavaScript to 300+ servers around the world. When someone visits the site, Cloudflare delivers the nearest copy instead of going all the way to the origin server.
Que se necesita: cambiar los nameservers del dominio ecstaticcrafts.com en el registrador (donde se compro el dominio) a los nameservers de Cloudflare. Esto no afecta el email ni ningun otro servicio — solo cambia quien resuelve el DNS.
What's needed: change the nameservers for ecstaticcrafts.com at the registrar (where the domain was purchased) to Cloudflare's nameservers. This doesn't affect email or any other service — it only changes who resolves DNS.
Ademas resuelve automaticamente:Also automatically fixes:
| HTTP/2 y HTTP/3HTTP/2 and HTTP/3 | Activado automaticamente — los 59 scripts cargan en paralelo en vez de en colaAutomatically enabled — 59 scripts load in parallel instead of queuing |
| TLS 1.1 | Un toggle en Cloudflare: Version minima TLS → 1.2. Resuelve el hallazgo S1.One toggle in Cloudflare: Minimum TLS Version → 1.2. Fixes finding S1. |
| Encabezados de seguridadSecurity headers | Cloudflare puede inyectar HSTS, X-Frame-Options, etc. sin depender de mod_headers del servidorCloudflare can inject HSTS, X-Frame-Options, etc. without depending on server mod_headers |
| Compresion BrotliBrotli compression | Mas eficiente que gzip — reduce el tamano de transferencia ~15-20% adicionalMore efficient than gzip — reduces transfer size ~15-20% more |
| Proteccion DDoSDDoS protection | Incluido gratis — protege contra ataques de denegacion de servicioIncluded free — protects against denial of service attacks |
El servidor actual (IP 67.23.63.136, nameservers propios ns1/ns2.ecstaticcrafts.com) es probablemente hosting compartido en un solo servidor. Incluso archivos HTML estaticos toman 1.3s en ser entregados. Un hosting administrado de WordPress usa infraestructura optimizada especificamente para WordPress + WooCommerce.
The current server (IP 67.23.63.136, self-hosted nameservers ns1/ns2.ecstaticcrafts.com) is likely shared hosting on a single box. Even static HTML files take 1.3s to deliver. Managed WordPress hosting uses infrastructure specifically optimized for WordPress + WooCommerce.
| Host | PrecioPrice | Por queWhy |
|---|---|---|
| Cloudways | ~$14/mo | Mejor relacion costo/rendimiento. DigitalOcean o Vultr. Varnish + CDN incluido.Best cost/performance ratio. DigitalOcean or Vultr. Varnish + CDN included. |
| SiteGround GoGeek | ~$15/mo | Buen soporte WooCommerce, CDN y cache incluidos.Good WooCommerce support, CDN and caching included. |
| Kinsta | ~$35/mo | Premium. Infraestructura Google Cloud. CDN incluido.Premium. Google Cloud infrastructure. CDN included. |
| WP Engine | ~$25/mo | Planes optimizados para WooCommerce.WooCommerce-optimized plans. |
Migracion: Se instala el plugin "All-in-One WP Migration" o "Duplicator", se exporta el sitio, se importa en el nuevo hosting, y se actualizan los nameservers. Tarda aproximadamente 1 hora. Migration: Install the "All-in-One WP Migration" or "Duplicator" plugin, export the site, import on the new host, and update nameservers. Takes approximately 1 hour.
Instalar el plugin gratuito "Asset CleanUp: Page Speed Booster" y descargar scripts de WooCommerce, FormCraft, YITH Wishlist y Variation Swatches en paginas que no los necesitan (homepage, blog, about). Esto no mejora el TTFB pero reduce el tiempo de renderizado despues de que el HTML llega al navegador. Install the free "Asset CleanUp: Page Speed Booster" plugin and unload WooCommerce, FormCraft, YITH Wishlist, and Variation Swatches scripts on pages that don't need them (homepage, blog, about). This won't improve TTFB but reduces render time after the HTML reaches the browser.
| ItemItem | Que se necesitaWhat's needed |
|---|---|
| Eliminar log de MerlinDelete Merlin log | Acceso FTP/cPanel: eliminar /wp-content/uploads/merlin-wp/ completoFTP/cPanel access: delete /wp-content/uploads/merlin-wp/ entirely |
| Desactivar wp-cron.phpDisable wp-cron.php | Agregar define('DISABLE_WP_CRON', true); a wp-config.php + crear cron del servidorAdd define('DISABLE_WP_CRON', true); to wp-config.php + set up server cron |
| Encabezados de seguridadSecurity headers | Las reglas .htaccess se agregaron pero mod_headers no esta habilitado en el servidor. Requiere que el proveedor de hosting lo active, o usar Cloudflare (que los inyecta a nivel de edge).The .htaccess rules were added but mod_headers is not enabled on the server. Requires the hosting provider to enable it, or use Cloudflare (which injects them at edge level). |
| TLS 1.1 | Requiere cambio en la configuracion de Apache (SSLProtocol) — acceso root o cPanel SSL, o Cloudflare.Requires change in Apache config (SSLProtocol) — root or cPanel SSL access, or Cloudflare. |
Material de apoyo para tener una conversacion con Zuliatec. No es acusacion — son hechos verificables y ocho preguntas que cualquier proveedor competente responde en cinco minutos. Supporting material for a conversation with Zuliatec. Not an accusation — just verifiable facts and eight questions any competent provider answers in five minutes.
El sitio estaba operando a menos del 30% de su potencial. Sin tocar el servidor — solo ajustando WordPress — en una sesion de trabajo: The site was operating at less than 30% of its potential. Without touching the server — just adjusting WordPress — in one work session:
| MetricaMetric | AntesBefore | DespuesAfter | MejoraImprovement |
|---|---|---|---|
| Tiempo al primer byteTime to first byte | 3.82s | ~1.2s | 69% |
| Carga totalTotal load time | 4.5s | ~1.5s | 67% |
| Plugins activosActive plugins | ~55 | ~25 | 55% menosfewer |
| Hallazgos de seguridad corregidosSecurity findings fixed | 0 | 10 | +10 |
Todos estos cambios son trabajo de WordPress, no de infraestructura. Un proveedor con monitoreo basico los habria identificado o recomendado. El sitio opero a un tercio de su capacidad durante anos. All these changes are WordPress work, not infrastructure. A provider with basic monitoring would have flagged or recommended them. The site ran at a third of its capacity for years.
No es cloud. No es servidor dedicado. Es hosting compartido (cPanel) en un data center rentado (Telx, Nueva York). En el mismo bloque IP hay 7 otros servidores, administrados por distintos propietarios, con distintos niveles de mantenimiento. Uno de ellos es un aparato VoIP ruso de 2007 con certificado SSL vencido en 2017 y mas de 180 vulnerabilidades sin parchar. Nadie lo ha tocado en nueve anos. La ausencia de inventario y auditoria del vecindario dice mas del proveedor que cualquier adjetivo. It's not cloud. It's not dedicated. It's cPanel shared hosting in a rented data center (Telx, New York). Seven other servers share the same IP block, owned by different parties with different maintenance levels. One of them is a Russian VoIP appliance from 2007 with an SSL certificate that expired in 2017 and 180+ unpatched vulnerabilities. Nobody has touched it in nine years. The absence of inventory and neighborhood auditing says more about the provider than any adjective could.
zuliatec.com tarda 3.01 segundos en responder — la misma franja donde estaba ecstaticcrafts.com antes de la optimizacion (3.82s). Es poco convincente como carta de presentacion de una empresa que vende hosting. zuliatec.com takes 3.01 seconds to respond — the same range ecstaticcrafts.com was in before optimization (3.82s). Not a convincing calling card for a company that sells hosting.
Zuliatec es a la vez la agencia que construyo el sitio y el proveedor del servidor donde vive. No es mala fe — es comun en agencias pequenas — pero significa que no hay un tercero que distinga entre "el sitio es lento" y "el hosting es lento". Las quejas llegan al mismo escritorio que decide cuanto hardware asignar. Zuliatec is both the agency that built the site and the provider of the server it lives on. Not bad faith — common in small agencies — but it means no third party can distinguish "the site is slow" from "the hosting is slow". Complaints arrive at the same desk that decides how much hardware to allocate.
DNS, web y correo viven en la misma maquina con el mismo IP (67.23.63.136). Los dos "servidores" DNS (ns1 y ns2) son la misma IP. Si la maquina cae: se cae el sitio, se cae el correo, se cae la resolucion del dominio — todo a la vez. Es la configuracion mas barata posible, y se nota. DNS, web, and mail all live on the same machine with the same IP (67.23.63.136). The two "DNS servers" (ns1 and ns2) are the same IP. If the machine goes down: site down, email down, domain resolution down — all at once. It's the cheapest possible setup, and it shows.
Escribimos reglas .htaccess para agregar encabezados de seguridad HTTP (HSTS, X-Frame-Options, CSP). No funcionaron porque el modulo mod_headers de Apache no esta habilitado en el servidor. Habilitarlo es una linea de configuracion. Un hosting serio lo trae activo por defecto desde hace mas de una decada.
We wrote .htaccess rules to add HTTP security headers (HSTS, X-Frame-Options, CSP). They don't work because the Apache mod_headers module is not enabled on the server. Enabling it is a one-line config change. Serious hosting has had this on by default for over a decade.
Preguntas que cualquier proveedor competente responde en cinco minutos. Las respuestas — o los silencios — son la informacion. Questions any competent provider can answer in five minutes. The answers — or the silences — are the information.
mod_headers en Apache?Can you enable mod_headers in Apache?| OpcionOption | Cuando tiene sentidoWhen it makes sense | CostoCost |
|---|---|---|
| A · Quedarse y presionarStay and pressure | Si Zuliatec responde bien las preguntas, ofrece un plan superior y se compromete con mejoras en tiempo.If Zuliatec answers the questions well, offers a higher tier, and commits to time-bound improvements. | $0 |
| B · Quedarse + CloudflareStay + Cloudflare | Cloudflare gratis resuelve ~80% de lo residual (velocidad, TLS 1.1, encabezados de seguridad) sin tocar la relacion con Zuliatec.Free Cloudflare handles ~80% of residual issues (speed, TLS 1.1, security headers) without touching the Zuliatec relationship. | $0 |
| C · Separar DNSSeparate DNS | Mueve el DNS a Cloudflare sin migrar el hosting. Elimina el punto unico de fallo y da respaldo si el servidor cae.Move DNS to Cloudflare without migrating the hosting. Removes the single point of failure and provides fallback if the server dies. | $0 |
| D · Migracion a hosting administradoMigrate to managed hosting | Si Zuliatec no responde bien, no tiene tier superior, o no hay SLA. Cloudways (~$14/mes), SiteGround GoGeek (~$15/mes), Kinsta (~$35/mes).If Zuliatec doesn't answer well, has no higher tier, or no SLA. Cloudways (~$14/mo), SiteGround GoGeek (~$15/mo), Kinsta (~$35/mo). | $14–35/mesmo |
Opcion B esta semana: agregar Cloudflare (gratis, 15 minutos, no rompe nada). Resuelve ~80% del problema residual sin tocar el hosting. En paralelo, enviar las ocho preguntas a Zuliatec por escrito con 72 horas de plazo. Las respuestas aclaran si corresponde A o D. Option B this week: add Cloudflare (free, 15 minutes, doesn't break anything). Handles ~80% of residual problems without touching hosting. In parallel, send the eight questions to Zuliatec in writing with a 72-hour deadline. The answers clarify whether A or D is the right path.
Escaneo externo de infraestructura con herramientas especializadas: nmap, subfinder, nuclei, wpscan, wafw00f, sslscan, testssl.sh. Sin explotacion, sin ataques de credenciales. Solo reconocimiento a profundidad. Velocidad limitada a 2-5 req/s para no afectar produccion. External infrastructure scanning with specialized tools: nmap, subfinder, nuclei, wpscan, wafw00f, sslscan, testssl.sh. No exploitation, no credential attacks. Deep reconnaissance only. Throttled at 2-5 req/s to avoid impacting production.
El sitio de WordPress es la fachada. Detras del puerto 443 hay un segundo servidor en el puerto 8000 — una aplicacion Django abandonada con DEBUG=True, Python 3.6 (sin parches desde 2021), y un panel de admin accesible a cualquiera. Es como tener una casa bien cerrada con llave mientras la cochera tiene la puerta abierta de par en par. The WordPress site is the storefront. Behind port 443 there's a second server on port 8000 — an abandoned Django application with DEBUG=True, Python 3.6 (no patches since 2021), and an admin panel accessible to anyone. It's a well-locked house with the garage door wide open.
Puerto 8000 corre una aplicacion Django via gunicorn 20.1.0. DEBUG=True en produccion. Cada error devuelve un traceback completo con rutas del servidor, version de Python, y configuracion del venv. Python 3.6 no recibe parches de seguridad desde diciembre 2021.
Port 8000 runs a Django application via gunicorn 20.1.0. DEBUG=True in production. Every error returns a full traceback with server paths, Python version, and venv configuration. Python 3.6 has received no security patches since December 2021.
DEBUG=False y migrar a Python 3.12+.
Shut down the gunicorn service on port 8000 immediately. If the Django API isn't in use (the React app that consumed it is dead), there's no reason for it to keep running. If it's needed, DEBUG=False and migrate to Python 3.12+.
La pagina de login del admin de Django esta abierta en :8000/admin/. Sin restriccion de IP, sin VPN, sin 2FA. Combinado con S11 (DEBUG=True en el mismo puerto), un atacante puede intentar credenciales y ademas ver la traza completa de cada error.
The Django admin login page is open at :8000/admin/. No IP restriction, no VPN, no 2FA. Combined with S11 (DEBUG=True on the same port), an attacker can attempt credentials and also see the full traceback on every error.
/admin/ por IP en el firewall o en la configuracion de gunicorn/nginx.
If the Django service is shut down (S11 fix), this resolves automatically. If it must stay, restrict /admin/ by IP in the firewall or gunicorn/nginx config.
MySQL escucha en 0.0.0.0:3306 y responde a conexiones TCP desde cualquier IP. La autenticacion es por whitelist de IP, pero el servicio sigue siendo alcanzable. Revela que la base de datos existe y esta activa. Vulnerable a DoS por inundacion de conexiones y a cualquier bug del protocolo de autenticacion de MySQL. MySQL listens on 0.0.0.0:3306 and responds to TCP connections from any IP. Authentication is IP-whitelist based, but the service is still reachable. Reveals the database exists and is active. Vulnerable to connection-flood DoS and any MySQL authentication protocol bug.
my.cnf, configurar bind-address = 127.0.0.1. Si hay aplicaciones remotas que necesitan acceso, usar un tunel SSH o VPN en vez de exponer el puerto directamente.
In my.cnf, set bind-address = 127.0.0.1. If remote applications need access, use an SSH tunnel or VPN instead of exposing the port directly.
Todos los subdominios resuelven a la misma IP: 67.23.63.136. WordPress, Django, React, cPanel, DNS, correo — todo en la misma caja. Si algo se compromete, todo se compromete. All subdomains resolve to the same IP: 67.23.63.136. WordPress, Django, React, cPanel, DNS, mail — all on the same box. If anything is compromised, everything is compromised.
7 puertos de correo abiertos: SMTP (25 filtrado), POP3 (110), IMAP (143), SMTPS (465), Submission (587), IMAPS (993), POP3S (995). Los banners revelan las versiones: Exim 4.95 y Dovecot. Exim tiene un historial extenso de RCEs criticos. 7 mail ports open: SMTP (25 filtered), POP3 (110), IMAP (143), SMTPS (465), Submission (587), IMAPS (993), POP3S (995). Banners reveal versions: Exim 4.95 and Dovecot. Exim has an extensive history of critical RCEs.
smtp_banner en la config de Exim).
If mail is handled via Google Workspace or another external provider, these services can be shut down on the server. If in use, update Exim and hide version banners (smtp_banner in Exim config).
OpenSSH 7.4 fue lanzado en diciembre 2016. RHEL a veces aplica parches de seguridad sin cambiar el numero de version, asi que puede ser menos severo de lo que el numero sugiere. Pero desde afuera no se puede verificar. OpenSSH 7.4 was released December 2016. RHEL sometimes backports security patches without updating the version string, so it may be less severe than the number suggests. But it can't be verified externally.
wafw00f no detecto ningun Web Application Firewall. AIOS funciona como plugin de seguridad de WordPress pero no opera a nivel de solicitudes HTTP — no filtra payloads maliciosos ni limita velocidad a nivel de red. wafw00f detected no Web Application Firewall. AIOS works as a WordPress security plugin but doesn't operate at the HTTP request level — it doesn't filter malicious payloads or rate-limit at the network layer.
En Fase 1 reportamos que mod_headers no estaba habilitado (S3). Error nuestro. Los subdominios (api., app.) SI devuelven los encabezados de seguridad que configuramos via .htaccess. El problema es que el dominio principal no los aplica — probablemente WP Rocket o algun plugin los esta descartando.
In Phase 1 we reported that mod_headers wasn't enabled (S3). Our mistake. The subdomains (api., app.) DO return the security headers we configured via .htaccess. The issue is the main domain doesn't apply them — likely WP Rocket or some plugin is discarding them.
.htaccess a la configuracion del vhost de Apache, donde ningun plugin puede tocarlos.
Review WP Rocket and Asset CleanUp configuration — one of them may be stripping HTTP headers. Alternative: move headers from .htaccess to Apache's vhost config, where no plugin can touch them.
app.ecstaticcrafts.com sirve una SPA de React 16 (create-react-app) que implementa un registro digital de propiedad de obras de arte: propietarios, artistas, obras, piezas (con codigos bidimensionales/QR), series, y un flujo de transferencia de propiedad entre coleccionistas. Aunque el backend esta roto (ver S11), la SPA sigue siendo publica y los source maps estan expuestos, lo que permite reconstruir el codigo fuente completo del frontend — 685 archivos originales, incluyendo 23 endpoints del API, la logica de autenticacion, y los nombres internos de los casos de uso.
app.ecstaticcrafts.com serves a React 16 SPA (create-react-app) implementing a digital art ownership registry: owners, artists, works, pieces (with 2D/QR codes), series, and an ownership-transfer workflow between collectors. Even though the backend is broken (see S11), the SPA is still public and its source maps are exposed, which lets an attacker reconstruct the entire frontend source — 685 original files including 23 API endpoints, authentication logic, and internal use-case names.
app. y api., eliminar los subdominios del DNS. Esto elimina S12, S21–S26 de un solo golpe. (2) Si se planea reactivar, primero: regenerar el build sin source maps (GENERATE_SOURCEMAP=false), mover la sesion de localStorage a cookies httpOnly, agregar integrity= a los scripts de unpkg (o mejor, auto-hostear), migrar a React 18+, y auditar el endpoint /post_send_email y /put_transferir_arte antes de re-exponerlos.
In order of preference: (1) if the app is truly abandoned, tear down the Apache vhost for app. and api. and remove the DNS records. This clears S12 and S21–S26 in one move. (2) If reactivation is planned, first: rebuild with no source maps (GENERATE_SOURCEMAP=false), move session from localStorage to httpOnly cookies, add integrity= on the unpkg scripts (or better, self-host them), migrate to React 18+, and audit /post_send_email and /put_transferir_arte before re-exposing them.
Los tres archivos .map de la SPA estan disponibles publicamente con 200 OK y contienen sourcesContent — es decir, cada archivo original .js, .tsx, CSS y SVG esta embebido textualmente. 685 archivos originales recuperados. Esto convierte un reconocimiento externo en un analisis cuasi-whitebox en cinco minutos.
All three .map files for the SPA return 200 OK and contain sourcesContent — i.e., every original .js, .tsx, CSS and SVG source file is embedded verbatim. 685 original source paths recovered. This turns external recon into near-whitebox analysis in five minutes.
GENERATE_SOURCEMAP=false antes de desplegar, o bloquear a nivel de Apache: <FilesMatch "\.map$"> Require all denied </FilesMatch>.
Rebuild with GENERATE_SOURCEMAP=false before deployment, or block at Apache: <FilesMatch "\.map$"> Require all denied </FilesMatch>.
localStorage — XSS equivale a robo de sesion
S22. Session stored in localStorage — XSS equals session hijack
El objeto de sesion (user, tipoActor, remember_me) se lee y escribe directamente en window.localStorage y window.sessionStorage. No se usan cookies httpOnly. Cualquier XSS en el dominio — o un compromiso del CDN externo (ver S23) — puede exfiltrar la sesion en una linea de JavaScript. Como la app maneja transferencias de propiedad de obras de arte, el robo de sesion equivale a una transferencia no autorizada.
The session object (user, tipoActor, remember_me) is read and written directly to window.localStorage and window.sessionStorage. No httpOnly cookies are used. Any XSS on the domain — or a compromise of the external CDN (see S23) — can exfiltrate the session in one line of JavaScript. Since the app handles art ownership transfers, session hijack equals unauthorized transfer.
localStorage.getItem("remember_me")sessionStorage.getItem("user") / localStorage.getItem("user")sessionStorage.getItem("tipoActor") / localStorage.getItem("tipoActor")httpOnly).
The house key is under the mat where any guest who steps in by mistake can pick it up. The right way is for the doorkeeper to keep it in his pocket and only let you in when you show your face (httpOnly cookies).
HttpOnly; Secure; SameSite=Lax. En localStorage dejar solo preferencias no sensibles (idioma, estado de UI).
When the app is reactivated, issue the session from the Django backend as an HttpOnly; Secure; SameSite=Lax cookie. Leave only non-sensitive preferences in localStorage (language, UI state).
El index.html carga las tres librerias principales desde unpkg.com con crossorigin pero sin integrity=. Si unpkg se compromete, o si un atacante intercepta la conexion, se ejecuta JavaScript arbitrario con todos los privilegios de la app — incluyendo acceso a los tokens de sesion en localStorage (S22). El tag @next en react-bootstrap es especialmente fragil: flota a versiones pre-release sin notificacion.
index.html loads all three core libraries from unpkg.com with crossorigin but without integrity=. If unpkg is compromised, or an attacker MITMs the connection, arbitrary JavaScript executes with full app privileges — including access to the session tokens in localStorage (S22). The @next tag on react-bootstrap is especially fragile: it floats to pre-release versions without notice.
<script src="https://unpkg.com/react/umd/react.production.min.js" crossorigin></script><script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js" crossorigin></script><script src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js" crossorigin></script>integrity= SHA-384.
Self-host the libraries (which is what create-react-app does by default — the CDN loads here look like a hand-edit to the template). If the CDN stays, pin exact versions and add SHA-384 integrity= hashes.
Cada llamada axios es recuperable del source map. La superficie del backend esta completamente mapeada sin necesidad de tocar api.ecstaticcrafts.com. Los endpoints notables para revisar cuando se restaure el backend: /get_env_variables (devuelve configuracion de entorno al cliente — patron clasico de disclosure accidental), /post_send_email (acepta HTML arbitrario y lista de destinatarios — posible open relay de correo si la autenticacion es debil), /put_transferir_arte (la transferencia de propiedad — la integridad del registro depende de la logica de autorizacion aqui).
Every axios call is recoverable from the source map. The backend surface is fully mapped without touching api.ecstaticcrafts.com. Notable endpoints to review when the backend is restored: /get_env_variables (returns environment config to the client — classic accidental-disclosure pattern), /post_send_email (accepts arbitrary HTML and recipient list — possible open mail relay if authentication is weak), /put_transferir_arte (the ownership transfer — registry integrity depends on the authorization logic here).
/asset-manifest.json publicamente servido — indice de todos los assets
S25. /asset-manifest.json publicly served — index of every static asset
Responde 4.8 KB de JSON listando cada ruta de asset estatico incluyendo los tres source maps y todos los iconos SVG. Elimina la necesidad de adivinar los nombres con hash de los bundles. Returns 4.8 KB of JSON listing every static asset path including the three source maps and every SVG icon. Removes the need to guess hash-suffixed bundle names.
FilesMatch de S21 o omitir del build. Misma solucion que S21 lo resuelve tambien.
Block with the same FilesMatch from S21 or omit from the build. Same fix as S21 resolves it too.
El bundle esta construido con React 16 (lanzado en 2017, soporte oficial terminado en 2022 cuando salio React 18) y create-react-app (deprecated oficialmente upstream en 2023). Ambos sin mantenimiento. Sin parches de seguridad upstream. Las dependencias transitivas en node_modules estan fijadas a versiones de 2021 (@coreui/react, react-router, axios, styled-components, date-fns) con CVEs publicas visibles.
The bundle is built with React 16 (released 2017, official support ended 2022 when React 18 shipped) and create-react-app (officially deprecated upstream in 2023). Both unmaintained. No upstream security patches. Transitive dependencies in node_modules are pinned to 2021-era versions (@coreui/react, react-router, axios, styled-components, date-fns) with published CVEs visible.
Con los source maps publicos (S21) extrajimos los 685 archivos fuente originales del bundle. 108 son codigo especifico de Ecstatic Crafts (el resto son node_modules). Revision manual linea-por-linea de las rutas sensibles (login, cambio de clave, transferencia, registro de arte). Aqui los hallazgos concretos — con referencias a archivo y numero de linea. Cada uno es explotable ahora (contra la logica del cliente) o lo sera cuando el backend Django vuelva a funcionar. Por claridad los agrupamos en 5 categorias: A) autenticacion/autorizacion, B) oraculos de enumeracion, C) confianza indebida en el cliente, D) facilitadores de phishing/spam, E) defaults criptograficos/diseño. With source maps public (S21) we extracted the 685 original source files from the bundle. 108 are Ecstatic Crafts-specific code (rest is node_modules). Manual line-by-line review of sensitive paths (login, change password, transfer, art registration). Below are the concrete findings — with file and line references. Each one is exploitable now (against client logic) or will be when the Django backend is restored. For clarity grouped into 5 categories: A) authentication/authorization, B) enumeration oracles, C) server-trust-client, D) phishing/spam enablers, E) cryptographic/design defaults.
El flujo de "cambiar contraseña" lee un ?token=XYZ de la URL y lo envia como Authorization: Token XYZ a PUT /put_user con la nueva clave. No hay verificacion de clave actual, no hay discriminacion entre tipos de token (reset vs sesion), no hay requisitos de complejidad (solo valida que los dos campos esten llenos y coincidan). Cualquier token de sesion robado sirve como token de reset. Cualquier token de reset filtrado otorga sesion permanente.
The "change password" flow reads ?token=XYZ from the URL and sends it as Authorization: Token XYZ to PUT /put_user with the new password. No current-password check, no token-type discrimination (reset vs session), no complexity requirements (only validates both fields non-empty and matching). Any stolen session token works as a reset token. Any leaked reset token grants permanent session.
components/CU0-Login/ChangePassword/index.js:102-112, 148-159api.put("/put_user", { clave: formFields.password }, { headers: { Authorization: tok }})
El window.history.pushState que limpia el ?token= de la URL corre dentro de un useEffect, o sea despues del primer render y despues de cargar los scripts externos de unpkg. El token queda en: logs de acceso de Apache, historial del navegador, URL de cualquier extension que observe pestañas. La cabecera Referrer-Policy protege contra fugas cross-origin, pero no contra logs ni historial.
The window.history.pushState that strips ?token= from the URL runs inside a useEffect — i.e., after initial render and after external unpkg scripts load. The token ends up in: Apache access logs, browser history, URL visible to any tab-reading extension. The Referrer-Policy header protects cross-origin leaks but not logs or history.
components/CU0-Login/ChangePassword/index.js:148-159useEffect(() => { ...query.get("token")... window.history.pushState(...) }, [])
replaceState sincronicamente al cargar la pagina (antes del useEffect), o mover el token a un cuerpo POST via formulario auto-submit al llegar al enlace.
Use replaceState synchronously on page load (before useEffect), or move the token to a POST body via auto-submit form when the link lands.
Cada llamada usa Authorization: Token ${user?.token}. Este es el patron por defecto de TokenAuthentication de Django REST Framework: un token por usuario, sin expiracion, sin rotacion, sin vinculacion a dispositivo. Combinado con localStorage (S22): roban el token una vez, acceden para siempre.
Every call uses Authorization: Token ${user?.token}. This is Django REST Framework's default TokenAuthentication pattern: one token per user, no expiration, no rotation, no device binding. Combined with localStorage (S22): steal the token once, access forever.
components/CU4.2-TransferirArte/index.js:28, y todos los componentes con llamadas autenticadasconst config = { headers: { Authorization: `Token ${user?.token}` }};
expiring-token de DRF y rotacion en cada login. Bindear al User-Agent o IP si es viable.
Migrate to JWT with TTL (15 min access + 7-day refresh), or at minimum implement DRF's expiring-token and rotation on every login. Bind to User-Agent or IP if feasible.
Despues del PUT /put_auth_user, el frontend llama GET /get_accesos_tipo_actor?grupo_actor=${u.grupo_actor}. El grupo_actor viene del response del login — pero se pasa como parametro de query en la siguiente llamada. Si el backend mira el query en vez de derivar del token: cualquier usuario autenticado puede pedir los permisos del grupo admin cambiando el parametro. Requiere verificacion server-side; el patron del cliente es el indicio.
After PUT /put_auth_user, the frontend calls GET /get_accesos_tipo_actor?grupo_actor=${u.grupo_actor}. The grupo_actor comes from the login response — but it's passed as a query parameter on the next call. If the backend looks at the query rather than deriving from the token: any authenticated user can request admin-group permissions by tampering the param. Requires server-side verification; the client pattern is the hint.
components/CU0-Login/index.js:212-220api.get("/get_accesos_tipo_actor", { params: { grupo_actor: u.grupo_actor }, headers: { Authorization: tok }})
grupo_actor desde el usuario resuelto por el token. Probar manualmente cambiando el param con DevTools para verificar.
In the backend, ignore the query param and read grupo_actor from the user resolved by the token. Test manually by tampering the param in DevTools to verify.
location.state (client-set)
SG-5. Admin flow decided by location.state (client-set)
En la transferencia de arte: if (!locationData?.admin) { putTransferirArte() } else { putPropietario() }. La accion "admin" (putPropietario, que edita perfiles de otros propietarios) se dispara basado en estado de navegacion del cliente — editable en DevTools. Requiere verificacion server-side del rol en cada endpoint sensible.
In the art transfer flow: if (!locationData?.admin) { putTransferirArte() } else { putPropietario() }. The admin action (putPropietario, editing other owners' profiles) is triggered based on client-held navigation state — editable in DevTools. Requires server-side role verification on every sensitive endpoint.
components/CU4.2-TransferirArte/index.js:91-96, 794-804
/put_propietario, /delete_*, etc.) debe verificar grupo_actor==1 del usuario autenticado, no confiar en cualquier dato del request.
Every admin-only endpoint (/put_propietario, /delete_*, etc.) must verify grupo_actor==1 from the authenticated user, not trust any request data.
HTTP 404 → "Correo electronico no encontrado". Exito → "Se envio a tu correo un enlace". Sin respuesta neutral. Alimenta cualquier email, obtienes si/no. No hay throttle visible del lado del cliente. HTTP 404 → "Email not found". Success → "A link was sent to your email". No neutral response. Feed any email, get yes/no. No throttle visible on the client.
components/CU0-Login/index.js:137-147
En el flujo de transferencia, HTTP 421 devuelve: "La direccion de correo electronico introducida pertenece a un administrador". Permite confirmar que emails son cuentas de admin — objetivo para ataques de credenciales o phishing dirigido. In the transfer flow, HTTP 421 returns: "The email address belongs to an administrator". Lets an attacker confirm which emails are admin accounts — targeting for credential attacks or spear-phishing.
components/CU4.2-TransferirArte/index.js:447-455
Tres codigos de error distintos en /post_registro_arte: 411 = pieza no encontrada, 412 = invitacion no encontrada, 413 = ambos validos pero no pertenecen a la misma pieza. Permite adivinar codigos de pieza e invitacion independientemente. Combinado con la validacion solo-alfanumerico (sin longitud minima, sin entropia requerida), el espacio de claves es atacable por fuerza bruta.
Three distinct error codes on /post_registro_arte: 411 = piece not found, 412 = invitation not found, 413 = both valid but don't belong to the same piece. Lets you brute piece codes and invitation codes independently. Combined with alphanumeric-only validation (no min length, no entropy required), the keyspace is brute-forceable.
components/CU4-RegistrandoArte/RegistroArte/index.js:262-288
/post_registro_arte. Forzar codigos con longitud minima 16 y entropia suficiente en el servidor.
Merge all three cases into a single generic error ("invalid codes"). Add rate-limit to /post_registro_arte. Enforce minimum-length 16 and sufficient entropy on codes server-side.
fk_propietario de estado de navegacion (IDOR potencial)
SG-9. Owner-read via fk_propietario from navigation state (potential IDOR)
El frontend envia id_propietario: locationData?.fk_propietario desde el estado de navegacion (manipulable en DevTools) a GET /get_propietarios. El servidor responde con el perfil completo del propietario: nombre, email, telefono, codigo de invitacion. Si el backend no verifica que el usuario autenticado tiene derecho a ver ese ID: IDOR clasico — cualquier usuario autenticado lee cualquier perfil.
The frontend sends id_propietario: locationData?.fk_propietario from navigation state (tamperable in DevTools) to GET /get_propietarios. The server returns the owner's full profile: name, email, phone, invitation code. If the backend doesn't verify the authenticated user has rights to view that ID: classic IDOR — any auth'd user reads any profile.
components/CU4.2-TransferirArte/index.js:594-599api.get(`/get_propietarios`, { ...config, params: { id_propietario: locationData?.fk_propietario }})
id_propietario. Test: autenticarse como usuario no-admin, tamperar fk_propietario, confirmar que retorna 403.
Server-side check: if user is not admin, can only request their own id_propietario. Test: authenticate as non-admin, tamper fk_propietario, confirm it returns 403.
id_propietario de navegacion (IDOR potencial)
SG-10. Owner-update with id_propietario from navigation (potential IDOR)
El putPropietario envia id_propietario: locationData?.fk_propietario y todos los campos editables (nombre, apellidos, celular, email, codigo_invitacion). Dispara por SG-5 (estado de navegacion decide admin). Si el servidor no re-verifica que el usuario puede editar ese ID, un no-admin con locationData.admin=true tamperado edita cualquier propietario.
putPropietario sends id_propietario: locationData?.fk_propietario and all editable fields (name, last names, mobile, email, invitation code). Triggered by SG-5 (nav state decides admin). If the server doesn't re-verify the user can edit that ID, a non-admin with tampered locationData.admin=true edits any owner.
components/CU4.2-TransferirArte/index.js:670-684
La funcion returnStringHTML construye un <html>...</html> completo del lado del cliente — con nombres, piezas, codigo de invitacion, y logo embebido. Ese HTML va como content al POST a /put_transferir_arte, que lo reenvia via el gateway de correo. Mismo patron en hooks/sendEmail.js hacia /post_send_email. Si la autenticacion es debil o ausente en esos endpoints: plataforma de phishing lista para usar, pre-marcada como Ecstatic Crafts, con HTML totalmente controlado por el atacante.
The returnStringHTML function builds a complete <html>...</html> client-side — with names, pieces, invitation code, and embedded logo. That HTML is sent as content via POST to /put_transferir_arte, which relays it via the mail gateway. Same pattern in hooks/sendEmail.js → /post_send_email. If auth is weak or missing on those endpoints: turn-key phishing platform, pre-branded as Ecstatic Crafts, with HTML fully under attacker control.
components/CU4.2-TransferirArte/index.js:220-392, 414, hooks/sendEmail.js{ email_list: ["victima@gmail.com"], subject: "...", content: "<html>...phishing...</html>", content_type: "html", origin: "..." }
/post_send_email + allowlist de destinatarios (solo el email del usuario autenticado).
The backend must compose HTML from server-side templates, not receive it from the client. If the template needs dynamic data, accept only parametric fields (name, code) and render into a fixed template. And: require strict auth on /post_send_email + recipient allowlist (only the authenticated user's email).
El codigo de invitacion es la credencial que otorga al receptor el derecho de reclamar la obra. Se renderiza como texto plano en el HTML del correo (${f.codigo_invitacion}). Termina en: logs del servidor de correo, cache de buzones, reenvios de email, backups de Gmail. Cualquiera con acceso al email tiene autorizacion de transferencia.
The invitation code is the credential that grants the recipient the right to claim the artwork. It's rendered plaintext in the email HTML (${f.codigo_invitacion}). Ends up in: mail server logs, mailbox caches, email forwards, Gmail backups. Anyone with email access has transfer authorization.
components/CU4.2-TransferirArte/index.js:369
app.ec.../claim?token=HASH) que el servidor intercambia por el codigo real al abrirse. El enlace en logs no sirve despues del primer uso.
Instead of sending the raw code, send a single-use link with TTL (e.g. app.ec.../claim?token=HASH) that the server exchanges for the real code when opened. The link in logs is useless after first use.
En la transferencia, codigo_invitacion y codigo_invitacion_generado reciben el MISMO valor del input del usuario — ambos se envian identicos al backend. No hay longitud minima, no hay entropia requerida, solo validationIsAlphaNumeric. Un propietario saliente malicioso puede setear codigo_invitacion = "1" — y el nuevo enlace es trivialmente adivinable. Sin TTL ni use-once visible en el cliente.
In the transfer, codigo_invitacion and codigo_invitacion_generado receive the SAME value from user input — both sent identical to the backend. No min length, no entropy required, only validationIsAlphaNumeric. A malicious outgoing owner can set codigo_invitacion = "1" — and the new link is trivially guessable. No TTL or single-use semantics visible on the client.
components/CU4.2-TransferirArte/index.js:406-407, 190-194codigo_invitacion: f.codigo_invitacion, codigo_invitacion_generado: f.codigo_invitacion
Las respuestas del servidor incluyen X-Frame-Options, X-Content-Type-Options, Referrer-Policy y Permissions-Policy — pero CERO CSP. Combinado con tokens en localStorage (S22) y scripts de unpkg sin SRI (S23): cualquier XSS puede exfiltrar tokens a cualquier dominio sin restriccion.
Server responses include X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy — but ZERO CSP. Combined with tokens in localStorage (S22) and unpkg scripts without SRI (S23): any XSS can exfiltrate tokens to any domain with no restriction.
evidence/phase-2/subdomains/app.headers.txt
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://unpkg.com; connect-src 'self' https://api.ecstaticcrafts.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com". Modo report-only primero para calibrar.
Add in Apache: Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://unpkg.com; connect-src 'self' https://api.ecstaticcrafts.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com". Report-only mode first to calibrate.
La ruta /consultar-artes auto-autentica al usuario si el estado de navegacion trae { email, pin, autoLogin: true }. Cualquier flujo interno que navegue con esos campos inicia sesion sin click. El riesgo depende de donde se pueblan esos campos — si viene de un enlace en un email (email + pin en URL params), entonces el enlace es una credencial transmisible.
The /consultar-artes route auto-authenticates if navigation state carries { email, pin, autoLogin: true }. Any internal flow that navigates with those fields logs in without a click. Risk depends on where those fields are populated — if it comes from an email link (email + pin in URL params), the link becomes a transferable credential.
components/CU0-Login/index.js:38-40, 296-298
location.state.dataCache con credenciales. Si viene de enlaces en email, migrar a tokens one-time (SG-12 fix aplica aqui tambien).
Audit where location.state.dataCache gets populated with credentials. If it comes from email links, migrate to one-time tokens (SG-12 fix applies here too).
Portal.js es un componente vacio
SG-16. Portal.js is an empty component
La ruta /portal renderiza <Content></Content> — un div estilizado vacio. No es explotable. Indicador de superficie obsoleta / flujo a medio implementar. Senala que la base de codigo tiene areas olvidadas — donde suelen esconderse otras suposiciones rotas.
The /portal route renders <Content></Content> — an empty styled div. Not exploitable. Marker of stale surface area / half-implemented flow. Signals this codebase has forgotten areas — where other broken assumptions tend to hide.
components/CU0-Login/Portal.js
16 hallazgos concretos con referencias a archivo y linea. Cuatro categorias de riesgo: autenticacion debil (SG-1 a SG-5), oraculos de enumeracion (SG-6 a SG-8), IDOR potencial (SG-9, SG-10), y spam/phishing (SG-11 a SG-13). La solucion de raiz — desmontar la app si esta abandonada — cierra los 16. Si se planea reactivar, cada uno requiere fix explicito antes de re-exponer. La buena noticia: el codigo no tiene dangerouslySetInnerHTML, eval, ni console.log de produccion. La mala: las vulnerabilidades no son bugs accidentales — son decisiones de arquitectura que confian demasiado en el cliente.
16 concrete findings with file and line references. Four risk categories: weak authentication (SG-1 through SG-5), enumeration oracles (SG-6 through SG-8), potential IDOR (SG-9, SG-10), and spam/phishing (SG-11 through SG-13). The root-cause fix — decommission the app if abandoned — closes all 16. If reactivation is planned, each one needs an explicit fix before re-exposing. Good news: the code has no dangerouslySetInnerHTML, no eval, no production console.log. Bad news: the vulnerabilities aren't accidental bugs — they're architectural decisions that over-trust the client.
Puerto 111 TCP+UDP corriendo rpcbind (Sun RPC portmapper). Servicios RPC no deberian estar expuestos a internet — se usan para NFS y servicios internos. Puede contribuir a ataques de amplificacion DDoS. Port 111 TCP+UDP running rpcbind (Sun RPC portmapper). RPC services should not be exposed to the internet — they're used for NFS and internal services. Can contribute to DDoS amplification attacks.
iptables/firewalld) o deshabilitar el servicio si NFS no se usa (systemctl disable rpcbind).
Block in the firewall (iptables/firewalld) or disable the service if NFS isn't used (systemctl disable rpcbind).
Puerto 53 abierto con respuesta NOTIMP. El servidor DNS es autoritativo para el dominio — esto es normal. El riesgo seria si ademas funciona como resolver recursivo abierto (vector de amplificacion DNS), pero nmap devolvio NOTIMP, lo que sugiere que no lo es. Port 53 open with NOTIMP response. The DNS server is authoritative for the domain — this is normal. The risk would be if it also functions as an open recursive resolver (DNS amplification vector), but nmap returned NOTIMP, suggesting it doesn't.
WordPress 6.9.4 (ultimo), tema Goya 1.0.9.6 (ultimo). Enumeracion de plugins bloqueada por directory listing deshabilitado (bien — resultado de Fase 2). Un usuario enumerado: web2021 (via patron de autor). Sin backups de configuracion encontrados.
WordPress 6.9.4 (latest), Goya theme 1.0.9.6 (latest). Plugin enumeration blocked by disabled directory listing (good — result of Phase 2). One user enumerated: web2021 (via author pattern). No configuration backups found.
La prueba de TLS en Fase 1 mostro TLS 1.1 aun habilitado (parcial). Fase 3 confirma: tanto TLS 1.0 como TLS 1.1 estan ahora bloqueados. Solo TLSv1.2 y TLSv1.3 aceptados. Todos los cipher suites grado A. Sin Heartbleed (verificado por sslscan). El hallazgo S1 esta ahora completamente resuelto. The TLS test in Phase 1 showed TLS 1.1 still enabled (partial). Phase 3 confirms: both TLS 1.0 and TLS 1.1 are now blocked. Only TLSv1.2 and TLSv1.3 accepted. All cipher suites grade A. No Heartbleed (verified by sslscan). Finding S1 is now fully resolved.
bind-address = 127.0.0.1 en my.cnf. Reiniciar MySQL.
bind-address = 127.0.0.1 in my.cnf. Restart MySQL.
app. y api. si la app React/Django no se usa. Reducir superficie de ataque.
Remove the app. and api. vhosts if the React/Django app isn't in use. Reduce attack surface.
| HerramientaTool | PropositoPurpose |
|---|---|
| nmap | Escaneo de puertos top 1000, deteccion de servicios, enumeracion TLSTop 1000 port scan, service detection, TLS enumeration |
| subfinder | Enumeracion de subdominiosSubdomain enumeration |
| nuclei | Deteccion de vulnerabilidades conocidasKnown vulnerability detection |
| wpscan | Escaneo especifico de WordPressWordPress-specific scanning |
| wafw00f | Deteccion de WAFWAF detection |
| sslscan | Verificacion de configuracion TLSTLS configuration verification |
| testssl.sh | Auditoria TLS/SSL exhaustivaComprehensive TLS/SSL audit |
Fase 1 (2026-04-09): Todas las mediciones se tomaron desde Moon (Pop!_OS, region Panama). Rendimiento: curl para temporalizacion y encabezados, parseo raw del HTML para conteo de activos y deteccion de plugins, tres ejecuciones consecutivas para validar la consistencia del TTFB. Seguridad: Reconocimiento externo unicamente — analisis de encabezados HTTP, enumeracion de directorios, sondeo de archivos, pruebas de protocolo TLS (OpenSSL), descubrimiento de rutas de la REST API, enumeracion de archivos de autor, fingerprinting de versiones. Sin pruebas autenticadas, sin explotacion, sin ataques de credenciales, sin fuzzing.
Fase 2 (2026-04-10): Sesion de optimizacion con acceso administrativo a WordPress. Se configuro WP Rocket (cache, JS diferido, LazyLoad, heartbeat, limpieza de base de datos, precarga), se eliminaron ~36 plugins (~24 inactivos + 12 activos innecesarios), se aplicaron reglas .htaccess via AIOS (Options -Indexes, bloqueo de install.php, rewrite de author enumeration), se eliminaron archivos por defecto de WP, se deshabilito la edicion de PHP, y se activo la proteccion contra hotlinking. Se instalo Asset CleanUp (limpieza de HTML: emoji scripts, generator meta, oEmbed, RSD, wlwmanifest, REST API links; XML-RPC deshabilitado). Mediciones post-optimizacion confirmadas con multiples ejecuciones consecutivas de curl. TTFB final: ~1.2s (mejora del 69%).
Fase 3 (2026-04-11/12): Pentest de infraestructura desde Moon. Herramientas: nmap (top 1000, deteccion de servicios, enumeracion de cipher suites), subfinder (subdominios), nuclei (vulnerabilidades conocidas), wpscan (WordPress-especifico), wafw00f (deteccion de WAF), sslscan + testssl.sh (auditoria TLS). Velocidad limitada a 2-5 req/s. Sin explotacion, sin ataques de credenciales. Se descubrio un segundo stack de aplicaciones (Django/React) en los puertos 8000 y subdominio app — no formaba parte del alcance original pero representaba el mayor riesgo encontrado.
Phase 1 (2026-04-09): All measurements taken from Moon (Pop!_OS, Panama region). Performance: curl for timing and headers, raw HTML parsing for asset counts and plugin detection, three consecutive runs to validate TTFB consistency. Security: External-only reconnaissance — HTTP header analysis, directory enumeration, file probing, TLS protocol testing (OpenSSL), REST API route discovery, author archive enumeration, version fingerprinting. No authenticated testing, no exploitation, no credential attacks, no fuzzing.
Phase 2 (2026-04-10): Optimization session with WordPress admin access. Configured WP Rocket (cache, deferred JS, LazyLoad, heartbeat, database cleanup, preload), removed ~36 plugins (~24 inactive + 12 unnecessary active), applied .htaccess rules via AIOS (Options -Indexes, install.php block, author enumeration rewrite), deleted WP default files, disabled PHP editing, and enabled hotlink protection. Installed Asset CleanUp (HTML cleanup: emoji scripts, generator meta, oEmbed, RSD, wlwmanifest, REST API links; XML-RPC disabled). Post-optimization measurements confirmed with multiple consecutive curl runs. Final TTFB: ~1.2s (69% improvement).
Phase 3 (2026-04-11/12): Infrastructure pentest from Moon. Tools: nmap (top 1000, service detection, cipher suite enumeration), subfinder (subdomains), nuclei (known vulnerabilities), wpscan (WordPress-specific), wafw00f (WAF detection), sslscan + testssl.sh (TLS audit). Throttled at 2-5 req/s. No exploitation, no credential attacks. Discovered a second application stack (Django/React) on port 8000 and app subdomain — not part of the original scope but represented the highest risk found.