Ciberseguridad · Autenticación · OAuth · Privilegio mínimo
Bloquear la puerta trasera: cómo seguí el acceso de administrador de mi sitio web
Quienquiera que controle el panel de administrador de un sitio web controla lo que cada visitante ve y ejecuta. Aquí está cómo funciona el administrador de este sitio sin un servidor que pueda ser vulnerado, por qué su clave solo abre una puerta y la brecha en la cadena de suministro que todavía necesito cerrar.
La puerta principal de un sitio web es la parte que ven los visitantes. El panel de administrador es la puerta trasera, y los atacantes lo saben. Quienquiera que controle el administrador controla lo que cada visitante lee, y en el peor de los casos, qué código se ejecuta en sus navegadores. Una página de inicio desfigurada es vergonzosa. Una página de inicio que sirve silenciosamente un script malicioso a todos los que la visitan es mucho peor.
Así que cuando agregué un editor a este sitio web, para que pueda publicar posts desde mi teléfono sin abrir un editor de código, traté su inicio de sesión como la parte más sensible de todo el sistema.

Decisión 1: no hay servidor que pueda ser vulnerado
La mayoría de los sistemas de gestión de contenido se ejecutan en un servidor con una base de datos de usuarios y contraseñas. Ese servidor se convierte en un objetivo: un plugin desactualizado, una contraseña débil o una base de datos expuesta pueden entregarle el sitio completo a un atacante.
Este sitio no tiene nada de eso. Es un sitio estático, reconstruido desde un repositorio de Git en GitHub cada vez que algo cambia. El panel de administrador (Sveltia CMS) es solo una aplicación de JavaScript que se ejecuta en mi navegador y habla directamente con la API de GitHub. Guardar un post significa hacer un commit.
Eso cambia la pregunta de seguridad. No hay servidor de administrador que parchear ni base de datos de contraseñas que filtrar. La verdadera pregunta se convierte en: ¿quién puede escribir en el repositorio? Todo lo demás sigue de la respuesta.
Decisión 2: una clave que abre una puerta
Para escribir en el repositorio, el CMS necesita un token de GitHub. La forma fácil de obtener uno es una aplicación clásica de OAuth con el repo alcance. Pero ese token podría leer y escribir cada repositorio al que mi cuenta puede acceder, público y privado. Si se filtra, el daño iría mucho más allá de este sitio web.
En su lugar, el inicio de sesión se realiza a través de una aplicación de GitHub que es:
- instalada en este repositorio solo, y
- concedida una sola permiso: Contenido (leer y escribir). No puede tocar problemas, ajustes, flujos de trabajo, otros repositorios o mi cuenta.
Esto es el principio de privilegio mínimo: dar a cada clave el menor poder que aún hace el trabajo, para que cuando (no si) algo sale mal, el radio de acción es pequeño.
Decisión 3: proteger la transferencia
El flujo de inicio de sesión de OAuth pasa por un pequeño Cloudflare Worker (el de código abierto sveltia-cms-auth, incorporado a mi repositorio sin modificar en un commit anclado para que sepa exactamente qué código se ejecuta). Contiene el secreto del cliente de la aplicación de GitHub como un secreto de Worker cifrado, nunca en el código ni en el repositorio. Tres de sus comprobaciones valen la pena explicar, porque cada una bloquea un ataque real:
- Lista de dominios permitidos. El Worker solo inicia un inicio de sesión para
festusboamah.com. Alguien que copia mi página de administrador en su propio dominio no puede usar mi ayudante de inicio de sesión. - Protección contra CSRF con
state. Antes de enviarme a GitHub, el Worker genera un valor aleatorio, lo almacena en unaHttpOnly,Securecookie, y lo pasa a GitHub como el parámetro OAuthstate. Cuando GitHub me devuelve, los dos deben coincidir. Esto evita que un atacante engañe a mi navegador para que termine una sesión de inicio que ellos iniciaron. - Entrega con verificación de origen. El token se devuelve a la ventana del CMS con
postMessage. El Worker solo lo libera cuando el origen informado por el navegador de la ventana que realiza la solicitud es mi dominio. Ese origen lo establece el navegador, no la página, por lo que no se puede falsificar.
Decisión 4: hacer que cada cambio sea responsable
Como cada guardado es un commit de Git en main, cada cambio en el sitio tiene un autor, una marca de tiempo y una diferencia exacta. En términos de STRIDE, esto aborda la negación: nada cambia en el sitio sin un registro. También me da una recuperación instantánea: si algo malo se publicara, puedo revertir el commit y Cloudflare Pages reconstruye la versión limpia en aproximadamente un minuto.
Las páginas de administración también obtienen sus propias cabeceras HTTP: una Política de Seguridad de Contenido separada, X-Robots-Tag: noindex para que los motores de búsqueda no las listen, y Cache-Control: no-store para que nada sensible se almacene en caché.
Donde todavía es débil
- El script del CMS no tiene comprobación de integridad. La página de administración carga Sveltia CMS desde el CDN unpkg. Establezco una versión exacta, lo que me protege contra actualizaciones sorpresa, pero no hay un hash de Integridad de Subrecursos. Si el CDN se viera comprometido y sirviera un archivo modificado, mi navegador lo ejecutaría con un token de GitHub válido al alcance. Esto es un riesgo clásico de cadena de suministro de software. La solución es un
integrityhash en la etiqueta del script o servir el archivo desde mi propio dominio. - La CSP de administración es más permisiva que la del sitio. Para cargar el CMS, la política de administración permite scripts desde unpkg y scripts en línea. Eso significa que si alguna vez existiera un bug de inyección en la administración, la política no detendría un script de cualquier paquete en unpkg. Alojar el CMS en mi propio dominio me permitiría ajustar esto también.
- Todo depende de una sola cuenta de GitHub. La aplicación limita lo que puede hacer un token, pero quien controle mi cuenta de GitHub controla el sitio. Eso hace que la autenticación de dos factores fuerte en esa cuenta (idealmente un pase o una clave de seguridad en lugar de SMS) sea el control más importante en todo este diseño, aunque no esté en el código en absoluto.
La lección
La decisión de seguridad más útil aquí no fue una comprobación astuta. Fue eliminar el servidor por completo, y luego hacer que la única clave restante sea lo más estrecha posible. Antes de agregar defensas, siempre vale la pena preguntar: ¿qué puedo eliminar, para que haya menos que defender?
¿Has detectado algo que me he perdido? Dímelo.
