El bug detrás del bug
Alguien me señaló que la sección “En GitHub” de este sitio mostraba íconos de imagen rota en vez de mis estadísticas y repos fijados. Pensé que sería un bug simple, probablemente algo de la CSP, ya que hacía poco había cerrado bien los encabezados de seguridad un par de issues atrás. No lo era. Pero arreglarlo sacó a la luz un segundo bug, peor, que no tenía nada que ver con GitHub.
El bug reportado: una demo muerta, no una config muerta. La tarjeta de estadísticas y las cuatro tarjetas de repos fijados cargan desde github-readme-stats.vercel.app, una herramienta pública y gratuita de la que dependen muchos perfiles de GitHub. Al hacerle curl directo devolvía 503 DEPLOYMENT_PAUSED, no un límite de tasa, no un tropiezo pasajero. Una búsqueda rápida encontró una serie de issues abiertos en el repo original que se remontan a enero: el mantenedor pausó el despliegue de la demo pública, y sigue pausado. El propio README del proyecto ya lo dice: el endpoint público es “best-effort,” y el arreglo recomendado es alojarlo uno mismo o generar las estadísticas vía GitHub Actions.
Ninguna de las dos opciones encajaba bien acá. Actions necesita un nivel de facturación para repos privados del que deliberadamente prescindo, y alojarlo yo mismo significa un segundo servicio que mantener para lo que es un widget decorativo. Busqué una alternativa genuina, algo que no fuera solo cambiar un único punto de fallo por otro idéntico, y no encontré ninguna. Cada opción es o “la demo gratis de otra persona” o “traer un token propio.” Terminé haciendo lo pragmático: cambiar el dominio a un fork activamente mantenido, github-stats-extended.vercel.app, que es un reemplazo directo para la misma API exacta. No es un arreglo permanente. Es el mismo riesgo con un mantenedor más sano detrás, y lo dejé escrito con claridad en vez de fingir lo contrario.
El bug que no estaba buscando. Mientras verificaba el arreglo en un navegador real, revisé la consola por hábito buscando violaciones de CSP, y encontré una que no tenía nada que ver con GitHub. Tiempo atrás había agregado una sola línea al script de lanzamiento del juego para accesibilidad de teclado: mover el foco al iframe del juego después del clic en el botón. Razonable, chica, sin relación obvia con seguridad.
Excepto que la CSP de este sitio fija ese script en línea exacto por su hash SHA-256, una forma legítima de permitir un script en línea específico sin abrir la puerta a ejecución en línea arbitraria. Cambiar un solo carácter del script cambia el hash. Nadie lo había recalculado. El build pasó, el typecheck pasó, el formato pasó, a ninguno de esos le importa ni sabe qué es un hash de CSP. El botón seguía viéndose normal. Pero al hacerle clic no pasaba nada, en silencio, en producción, desde el despliegue anterior.
Esa es la parte que se me quedó grabada. Una CSP fijada por hash es más segura que 'unsafe-inline', y volvería a defenderla. Pero también es una trampa sin alarma: no hay linter, no hay prueba, no hay paso de build que conecte el contenido de un script con la CSP que se supone debe permitirlo. La única forma en que lo atrapé fue por hábito, correr un navegador real contra un build real antes de dar algo por terminado, en vez de confiar en que nada había fallado.
Arreglé los dos en la misma pasada, recalculé el hash, y volví a verificar con la misma revisión de navegador headless. Esta vez dejé el modo de fallo bien escrito, con el comando real para recalcular el hash, para que la próxima vez que un ajuste de accesibilidad de una línea toque ese script, haya algo que refresque la memoria en vez de depender de atraparlo por accidente otra vez.
Lecturas relacionadas
Convertir un hash de CSP en una salida del build en vez de mantenerlo a mano
Un botón de lanzamiento del juego se rompió en silencio dos días antes de que alguien lo notara. El arreglo no fue recalcular un hash, fue asegurarse de que nadie tuviera que volver a hacerlo.
Un cambio de token de una línea que encontró un botón roto y un vacío de GDPR
Arreglar un token de analítica de marcador de posición se convirtió en una lección sobre cómo los hashes de CSP se vencen en silencio, y por qué un servidor de desarrollo puede mentir sobre el comportamiento en producción.
Tres formas en que "la spec decía X" estaba mal
Un destino de symlink muerto, una solución que la automatización ya tenía en cola, y documentación que describe componentes que no existen - tres fuentes de verdad, la misma lección cada vez.