Volver al Blog

REPLICATION nunca fue un permiso de solo lectura: PostgreSQL cerró un dlopen() de doce años

Dos servidores idénticos de rack sacados sobre sus guías en una sala de servidores pequeña

La cuenta que alguien pidió por ticket con la frase «solo tiene que leer el WAL» podía cargar cualquier fichero del sistema y ejecutar su código como el usuario que ejecuta la base de datos. No es una lectura nuestra: es la descripción oficial del CVE-2026-6471, publicada por el propio proyecto PostgreSQL el 13 de agosto. Estuvo así doce años.

Diseñamos y operamos infraestructura y plataformas de aplicaciones para clientes, y una base de datos con su réplica y algún consumidor de cambios colgando es una pieza de lo más corriente en esos montajes. Por eso este CVE nos interesa menos por el agujero —que se cierra actualizando— y más por dos cosas que se quedan después del parche: qué creía la gente que estaba concediendo cuando concedía ese permiso, y qué se rompe el día que lo actualizas sin leer la nota de versión.

Qué podía hacer exactamente una cuenta con REPLICATION

La descripción del proyecto es de las que no necesitan traducción interpretativa: «Missing authorization in PostgreSQL logical decoding allows a non-superuser holding REPLICATION privilege to dlopen any file visible to the operating system account running the server, via the choice of logical decoding plugin. This in turn runs arbitrary code as that account.»

El mecanismo cabe en dos frases. Cuando un cliente crea un slot de decodificación lógica, indica con qué plugin quiere que se le sirvan los cambios; ese nombre es un texto que el cliente elige. Hasta el parche, ese texto llegaba al cargador dinámico del sistema sin que nadie comprobara si el fichero al que apuntaba tenía algo que ver con PostgreSQL. Hace falta que el servidor tenga wal_level = logical —la replicación física no carga plugins— y hace falta el atributo REPLICATION en el rol. Nada más.

La investigación de Cyera, que es quien firma el análisis largo del fallo bajo el nombre PostGREShell, añade el detalle incómodo: el fichero no tiene por qué estar ya en la máquina. En Windows describen rutas UNC —basta alcanzar el 445 del atacante— y en el resto acotan el terreno: sistemas de la familia RHEL con autofs y macOS anteriores a Catalina, donde el automontaje de NFS viene puesto de serie. Y una vez el código corre como el usuario del sistema, describen el camino hasta superusuario de la base tocando catálogos directamente. Ese tramo es de ellos y así lo citamos: nosotros no lo hemos reproducido.

El manual llevaba doce años avisando; el ticket decía otra cosa

La documentación de CREATE ROLE no se anda con matices: «A role having the REPLICATION attribute is a very highly privileged role, and should only be used on roles actually used for replication.» Está ahí, en la página que consulta cualquiera que cree la cuenta. Y aun así, el atributo se concede a diario en organizaciones serias sin que nadie parpadee.

La razón es la de siempre, y no tiene que ver con la pereza: el permiso se pide por lo que hace y se concede por lo que se llama. Quien lo pide escribe «necesito montar la captura de cambios hacia el data warehouse» o «la herramienta de copia necesita leer el WAL». Quien lo aprueba lee «replicación» y entiende «lee, no escribe». Los dos tienen razón en su frase y los dos están equivocados sobre lo que acaba de pasar, porque el atributo no describe una operación, describe un canal: conectar en modo replicación y crear slots. Lo que hicieran los slots era, hasta agosto, otra conversación que no tuvo nadie.

Es el mismo patrón que contábamos en el caso del Gitea donde el permiso de escritura lo daba el formulario de registro: nadie decidió conceder aquello, se concedió solo porque el nombre de la casilla sonaba inofensivo.

El 7,2 no es el número que decide

El CVE puntúa 7,2, y el vector explica por qué no llega más arriba: PR:H, privilegios requeridos altos. La métrica está aplicando al pie de la letra lo que dice el manual —REPLICATION es un rol muy privilegiado—, así que en el papel el atacante ya tenía que ser alguien de confianza. En la práctica, ese atributo vive en cuentas de servicio: herramientas de copia, servidores en espera, pipelines de captura de cambios y sistemas de monitorización que leen el WAL, que es la lista que enumera Cyera. La puntuación describe la puerta con precisión; lo que no puede saber es cuántas llaves has repartido tú.

Y ahora la parte que no vamos a exagerar, porque tenemos la costumbre de mirar antes de meter prisa. A 4 de septiembre, The Hacker News comprobó dos cosas y publicó las dos: este CVE no estaba en el catálogo de vulnerabilidades explotadas de CISA, y no había código de prueba público en repositorios. Cyera, por su parte, dice haber encontrado 114 plugins maliciosos de PostgreSQL circulando —troyanos, mineros y reverse shells— en una búsqueda por VirusTotal, y es honesta con lo que eso significa: no vincula ninguno a la explotación de este CVE. Conviene subrayarlo, porque el titular se presta al salto: que existan librerías maliciosas para PostgreSQL demuestra que hay gente escribiéndolas y que el mecanismo de carga es un objetivo conocido desde antes. Es otra afirmación, y es la que hay. Lo mismo pasaba con los dos CVE de Artifactory de la semana pasada, donde el que entró antes en el catálogo de explotados fue el de nota media, no el crítico.

El parche mueve la frontera de sitio

La corrección no quita capacidades al rol: añade un parámetro de servidor. Se llama output_plugin_libraries y la documentación lo define como la lista de librerías instaladas en dynamic_library_path «that are also trusted for use as logical output plugins by replication clients». Cualquier petición que nombre otra cosa se rechaza, y la documentación añade una frase que conviene leer despacio porque incluye a los superusuarios: «All users are subject to this restriction.»

Merece la pena decir lo que eso significa en términos de gobierno, porque es la parte que no sale en ningún titular. Antes, quién podía hacer cargar código en el servidor lo decidía un atributo del rol, y los roles los crea y modifica bastante gente. Ahora lo decide una lista que solo tocan los superusuarios: en el código fuente, el parámetro está declarado PGC_SUSET y con la marca GUC_SUPERUSER_ONLY, así que ni siquiera se puede leer con una cuenta normal. En la práctica vive en postgresql.conf, pero sigue habiendo camino por SQL: un superusuario puede hacer ALTER SYSTEM SET output_plugin_libraries y recargar sin bajar a la máquina. Hay muchas menos manos ahí, y esa es la mejora real. Lo que no cambia es que el control se ha mudado a un sitio donde, si la configuración de la base de datos no está versionada ni la revisa nadie, tampoco mira nadie.

Y aquí es donde el parche de seguridad te para la replicación

El valor por defecto de la lista son dos nombres: pgoutput y test_decoding, los dos plugins que vienen con PostgreSQL. Las notas de la 18.6 lo dicen con todas las letras y con el aviso incluido: «Installations that rely on other output plugins must add them after updating the server». Traducido a consecuencias: si tu captura de cambios va por wal2json o por decoderbufs, al reiniciar con la versión nueva la decodificación lógica se niega a funcionar hasta que alguien edite la configuración.

El proyecto wal2json ya lo documenta en su propio README, con la línea que hay que escribir —output_plugin_libraries = 'pgoutput, test_decoding, wal2json'— y con el error que verás si no la escribes: library "wal2json" may not be used as an output plugin. La buena noticia operativa es que el parámetro admite recarga de configuración: pg_ctl reload o SELECT pg_reload_conf(), sin parar el servidor otra vez. Y el mensaje es de los buenos: la documentación muestra que en el log del servidor sale con ERROR, DETAIL —con el valor actual de la lista— y un HINT que te dice literalmente que añadas la librería y recargues. El problema no es que el sistema no lo diga; es que lo dice en el log de un servidor que a esa hora no está mirando nadie.

Sobre decoderbufs conviene ser preciso, porque es el que más despliegues puede llevarse por delante: la documentación del conector de PostgreSQL de Debezium lista decoderbufs y pgoutput como opciones de plugin.name y da decoderbufs como valor por defecto. Quien lo cambió a pgoutput a mano no se entera de nada. Quien lo dejó como venía, sí. Hay un segundo sitio donde salta, y este muerde en migraciones: pg_upgrade --check falla si el clúster nuevo no permite los plugins de los slots del viejo, cuando se migra desde la 17 o posterior. Es un fallo bienintencionado —te avisa antes— pero llega en el momento de menos gracia posible, con la ventana abierta y el reloj corriendo.

Todo esto viaja dentro de una actualización menor de seguridad, del tipo que muchas organizaciones aplican en automático por el canal de su distribución. Es la misma forma de romper producción que contamos en agosto con el parche de .NET que se llevó por delante la impresión de aplicaciones WPF, con una diferencia a favor de PostgreSQL que es de justicia reconocer: aquí el proyecto lo documenta, lo explica y te deja la salida escrita en una línea. Que además esa serie fuera de la 18.4 a la 18.6 porque la 18.5 se retiró por una regresión —lo dice el propio anuncio— es el resumen involuntario del asunto: las versiones menores no son trámite, ni siquiera para quien las publica.

De esa misma tanda del 13 de agosto —28 CVE de golpe— ya escribimos sobre otro que se comportaba igual de callado: pgcrypto guardando texto plano en la columna que creías cifrada. Dos fallos de la misma publicación, con la misma propiedad: ni el INSERT fallaba entonces ni el servidor grita ahora.

Cuatro consultas que contestan si esto te toca

Se hacen en cinco minutos por instancia y contestan las dos preguntas del artículo: a quién se lo diste y qué se te va a parar.

  • SELECT rolname FROM pg_roles WHERE rolreplication; — quién tiene el atributo hoy. A esa lista súmale mentalmente los superusuarios, que lo llevan implícito.
  • SHOW wal_level; — si no pone logical, la decodificación lógica no está en juego en esa instancia y este fallo concreto no te alcanza.
  • SELECT slot_name, plugin, active FROM pg_replication_slots; — la columna plugin te dice qué nombres tendrás que meter en la lista blanca. Esta consulta antes de actualizar es la diferencia entre una ventana aburrida y una llamada; de hecho, la propia documentación incluye una versión de ella para construir la lista a partir de los slots persistentes.
  • SHOW output_plugin_libraries; — después de actualizar, para ver qué permite el servidor de verdad. Ojo con quién la lanza: el parámetro está marcado como de solo superusuario, así que con la cuenta de la aplicación no verás el valor sino un error; hace falta superusuario o pertenecer a pg_read_all_settings. Y fuera de SQL: las líneas replication de tu pg_hba.conf, que es donde Cyera recomienda limitar esas conexiones a direcciones de confianza.

Queda una pregunta que no contesta ninguna consulta, y es la que de verdad define tu superficie después del parche: quién puede escribir en postgresql.conf y en el directorio de librerías de la máquina. Si la respuesta es «no lo sé» o «cualquiera con acceso al servidor», la lista blanca es una puerta con la llave puesta por dentro.

El orden en que lo haríamos

Primero el inventario de slots y plugins de cada instancia, incluidas las de preproducción —que son las que nadie mira y las que primero se actualizan solas—. Después, en el mismo cambio que sube la versión, la línea de output_plugin_libraries con lo que ya usas; no en un segundo cambio «cuando veamos que falla». Luego, quitar el atributo a lo que no replica: ALTER ROLE … NOREPLICATION, que es reversible en una línea, aplicado a las cuentas que salieron de la primera consulta y que nadie sabe explicar. Y por último la alerta, que es la parte que casi nunca está.

Insistimos en la alerta porque el coste de no enterarse no es solo que los datos no lleguen a su destino. Un slot que existe y no avanza retiene WAL en el disco del servidor, y ahí el reloj corre solo: primero es una gráfica que sube, después es una partición llena, y una partición llena en una base de datos no es un incidente de replicación, es una parada. Hay un freno para eso, max_slot_wal_keep_size, y conviene saber que viene de fábrica en -1: sin límite. Ponerle un valor cambia el modo de fallar —el slot se queda inservible en vez de llenarte el disco— y esa es una decisión que se toma antes, no a las tres de la madrugada. La alerta útil no es «el servicio está arriba», es el retraso del slot creciendo. Nosotros monitorizamos con Zabbix y la regla que seguimos es la misma desde hace años: alertas que importan, no ruido, y esta es de las que importan porque avisa de una parada con horas de margen. Si tienes alguien de guardia 24×7, esa métrica tiene que estar en su pantalla; si no lo tienes, tiene que estar en el correo de alguien que mire el domingo. Y en la 18 hay un tercer parámetro que conviene conocer antes de necesitarlo, idle_replication_slot_timeout, que llega desactivado.

Y una regla de conducta que sale gratis: el día que un proveedor añade un parámetro nuevo con valor por defecto restrictivo, esa versión no se aprueba sin leer la nota entera. Un parámetro nuevo con defecto restrictivo es, por definición, un cambio de comportamiento disfrazado de corrección de seguridad. Vale para PostgreSQL y vale para todo lo demás que corre en la plataforma de datos y aplicaciones de la que depende tu negocio.

Lo que queda cuando el parche ya está puesto

El agujero se cierra actualizando y no hay más misterio. Lo que no se cierra actualizando es el hábito que lo hizo interesante: conceder atributos por el nombre que tienen en el formulario en vez de por lo que permiten hacer. Doce años después, la respuesta correcta a «¿quién tiene REPLICATION?» sigue siendo, en demasiadas casas, «los de siempre, supongo». Zero Trust no es comprar un producto: empieza siendo capaz de contestar esa pregunta con una lista, y sigue por que la lista sea corta.

¿Sabes quién tiene REPLICATION en tus bases de datos?

Revisamos permisos de cuentas de servicio, ventanas de actualización y alertas de replicación como parte del trabajo, con enfoque Zero Trust: nada de confianza implícita por el nombre del rol. No somos resellers de nadie, así que la recomendación de quitar un permiso —o de dejarlo donde está— no nos beneficia de una manera u otra.

Hablar con everyWAN

Nota de fuentes

La descripción del CVE-2026-6471, la puntuación 7,2 con vector AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H y las versiones afectadas y corregidas (18.6, 17.11, 16.15, 15.19 y 14.24, publicadas el 13 de agosto de 2026) son de la página oficial del proyecto en postgresql.org/support/security. El crédito del hallazgo es el que publican las notas de la 18.6: «The PostgreSQL Project thanks Vladimir Tokarev and Yu Kunpeng for reporting this problem». La cita sobre el atributo REPLICATION es de la documentación de CREATE ROLE; la definición de output_plugin_libraries, su valor por defecto, el bloque de ERROR/DETAIL/HINT del log y la consulta para construir la lista salen de la documentación de configuración de replicación de PostgreSQL 18, igual que max_slot_wal_keep_size e idle_replication_slot_timeout. Que el parámetro sea PGC_SUSET con la marca GUC_SUPERUSER_ONLY lo hemos leído en src/backend/utils/misc/guc_tables.c de la rama 18 del repositorio de PostgreSQL, no en la documentación. La retirada de la 18.5 por una regresión está en el anuncio oficial del 13 de agosto. El texto sobre la lista blanca, el ejemplo de configuración y el aviso de pg_upgrade --check son de las notas de la versión 18.6. El error library "wal2json" may not be used as an output plugin y la línea de ejemplo con wal2json son del README del propio proyecto wal2json. Los valores de plugin.name del conector de PostgreSQL de Debezium son de su documentación. El nombre PostGREShell, el detalle de rutas UNC y automontaje NFS, la cifra de 114 plugins maliciosos en VirusTotal, la enumeración de herramientas que suelen llevar el atributo y la recomendación sobre pg_hba.conf son de la investigación publicada por Cyera, que no vincula esos plugins a este CVE; el subrayado de esa distinción es nuestro. La comprobación de que el CVE no figuraba en el catálogo KEV de CISA ni había prueba de concepto pública a 4 de septiembre de 2026 es de The Hacker News. Los doce años se cuentan desde la introducción de la decodificación lógica en PostgreSQL 9.4, publicada en diciembre de 2014, así que redondeamos al alza por unos meses. Las consultas SQL son estándar y cualquiera puede ejecutarlas contra su instancia para comprobar lo que decimos.

PostgreSQL Zero Trust Datos Parcheo
Compartir LinkedIn X

Suscríbete a nuestra newsletter

Para recibir historias del mundo IT, novedades de everyWAN y ofertas exclusivas para suscriptores, date de alta a nuestra lista de correo

everyWAN
everyWAN