log4j, las vulnerabilidades, los parches y la sobre-ingeniería

Durante estos días si te dedicas mínimamente a esto de la informática y has creado algún sistema con Java habras sufrido el problema del log4jshell. Si es que no, pues te lo explico:

log4j es una librería de Java que lleva existiendo desde hace innumerables años y que es muy utilizada para registrar los eventos que tienen lugar en un sistema y poder controlar qué se registra y qué no. Pues bien, la versión 2 de esta librería incluía funcionalidades para hacer sustituciones en las cadenas que se guardan en el log y, continuando con la tradición más añeja de Java, se les ocurrió que sería buena idea que esas cadenas a sustituir pudiesen hacer llamadas a sistemas externos y recibir objetos completos. Utilizando el estandar jndi permitían incluso hacer llamadas RMI o acceder a servidores LDAP. Supongo que el programador que añadió estas funcionalidades vería su utilidad en algún momento, pero también es cierto que yo nunca he visto a nadie usarla.

En fin, que esto llevaba ahí un tiempo y a algún «hacker malvado» se le ocurrió probar qué pasaba si en la url que le pasaba a la cadena a loguear incluía la dirección de un servidor que él controlaba y que permitía descargarse un objeto que hacía «cosas malas» en el sistema… Y la prueba funcionó y en cuanto alguien se dió cuenta que había cadenas extrañas que empezaban por ${jndi:ldap:// y que provocaban funcionamientos anómalos se dio la voz de alarma y se creó un registro de vulnerabilidad (CVE-2021-44228) y con un exploit super-sencillo y que mostraba que había cientos de miles de sistemas afectados.

El parche a esa vulnerabilidad llegó pronto (y eso que log4j está mantenido solo por voluntarios) y mucha gente se pasó buna parte del fin de semana parcheando los sistemas (alguno tardó algo más), aunque sin tener muy claro si el sistema había sido comprometido y modificado antes, cosa que requiere otras medidas adicionales y muy costosas. A día de hoy se ha encontrado que el parche que se hizo no contempla todos los casos y se ha registrado otra vulnerabilidad (CVE-2021-45046) que requiere otro parche… Parches, que, al fin y al cabo lo único que hacen es deshabilitar esa funcionalidad que, en algún momento, a alguien, le pareció interesante.

Son incontables el número de horas que el personal de IT (desde programadores hasta técnicos de sistemas) se han dedicado a tapar este agujero (y seguro que habrá muchos agujeros sin tapar todavía). Eso se traduce en muchos millones de Euros de dinero gastado sin sentido… Y todo porque a alguién «le pareció una buena idea esa funcionalidad» y porque todo el mundo usa componentes que no conoce, en los que «confía», aunque no poga un duro para su desarrollo…

Por cierto… Hay por ahí ya los «negacionistas de los parches» que van diciendo que si los parches son malos, que no saben lo que hay dentro, que todo es parte de una conspiración y esas cositas… En fin, el ser humano es lo que tiene (o no).

ACTUALIZACIÓN 17-12-2021: Hay todavía otro parche que meter, el 2.17.0 ya que hay una cadena especial que puede dar como resultado un DDoS… A ver lo que nos dura.

Subir a maven central una librería propia

Ahora que ya acabas de construir una librería interesante en Java, la has hecho pública (en github, por ejemplo) y quieres que todo el mundo la use… Queda una tarea pendiente, subirla a un repositorio maven para ponerla a disposición de los que utilicen este sistema (o gradle, que hoy en día ya son casi todos).

Vamos a verlo con un ejemplo que he subido esta mañana… Hay cosas que todavía no entiendo del todo, pero el resultado ha sido bueno, por lo que, al menos, podremos usar esta receta como guía para próximas veces.

El código que intento subir es una librería simple que tengo alojada en github con su pom.xml básico y que si te descargas el proyecto podrías compilar e instalar en tu maven con mvn install. La dirección es esta:

https://github.com/yoprogramo/nomorepass-java/

Ahora, para que todo el mundo pueda descargárselo como dependencia y no tenga que hacer el mvn install del proyecto, tenemos que subirlo a un repositorio público, podemos ver una guía en esta página: Guide to Public Maven Repositories. Tal como explican en la página, lo más sencillo para publicar en Maven Central es usar el repositorio Sonatype. Dicho y hecho… Lo intentamos por aquí.

Lo primero es crear una cuenta en el Jira de Sonatype aquí. Lo siguiente, y esto es un poco «tricky» es crear un ticket solicitando un nuevo id de grupo en esta dirección. No se puede pedir cualquier id de grupo (en mi caso quería pedir com.nomorepass) y generalmente se pedirá alguna prueba de que el dominio es tuyo. En mi caso este es el ticket que creé: https://issues.sonatype.org/browse/OSSRH-49426, para demostrar que el dominio era mío cambié el DNS e incluí una entrada TXT con el identificador del ticket:

Una vez autorizado (tarda un poco, es un proceso manual) hay que modificar nuestro código y prepararlo para la subida, pero, antes de eso, tenemos que generar nuestras claves gpg para poder firmar el código. eso se hace con este comando:

gpg --gen-key

Una vez generada podremos acceder a la lista de claves con el comando:

gpg --list-keys

Toma nota del id de la clave y recuerda la contraseña que usaste para generarla, porque tendrás que recordarla. Además, tendrás que publicarla en algún servidor de claves públicas para que pueda ser comprobada.

gpg --keyserver hkp://keys.gnupg.net --send-keys <el-id-de-la-clave>

Ahora empezamos a modificar el pom.xml para que cumpla con los requisitos para el repositorio Maven Central. En nuestro caso pusimos esto:

<groupId>com.nomorepass</groupId>
  <artifactId>nomorepass</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>

  <name>Nomorepass java library</name>
  <description>NoMorePass protocol 2 implemented on Java.</description>
  <url>https://nomorepass.com</url>

  <licenses>
    <license>
      <name>Apache License, Version 2.0</name>
      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
      <distribution>repo</distribution>
    </license>
  </licenses>

  <developers>
    <developer>
      <name>Jose Antonio Espinosa</name>
      <email>[email protected]</email>
      <organization>Nomorepass</organization>
      <organizationUrl>https://nomorepass.com</organizationUrl>
    </developer>
  </developers>

  <scm>
    <connection>scm:git:git://github.com/yoprogramo/nomorepass-java.git</connection>
    <developerConnection>scm:git:ssh://github.com:yoprogramo/nomorepass-java.git</developerConnection>
    <url>https://github.com/yoprogramo/nomorepass-java/tree/master</url>
</scm>

Y, una vez informado de todo esto, hay que incluir los plugins que nos permitirán hacer el despliegue directamente. Yo añadí esto:

<distributionManagement>
    <snapshotRepository>
      <id>ossrh</id>
      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </snapshotRepository>
    <repository>
      <id>ossrh</id>
      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
    </repository>
</distributionManagement>

Y puse en mi directorio de maven settings.xml los datos de mi usuario

<settings>
  <servers>
    <server>
      <id>ossrh</id>
      <username>xxxxxxxxxx</username>
      <password>xxxxxxxxxx</password>
    </server>
  </servers>
</settings>

Por último, toda la sección de build (que no tenía) la sustituí por esto:

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-gpg-plugin</artifactId>
        <executions>
          <execution>
            <id>sign-artifacts</id>
            <phase>verify</phase>
            <goals>
              <goal>sign</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.sonatype.plugins</groupId>
        <artifactId>nexus-staging-maven-plugin</artifactId>
        <version>1.6.7</version>
        <extensions>true</extensions>
        <configuration>
          <serverId>ossrh</serverId>
          <nexusUrl>https://oss.sonatype.org/</nexusUrl>
          <autoReleaseAfterClose>true</autoReleaseAfterClose>
        </configuration>
      </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-source-plugin</artifactId>
          <version>2.2.1</version>
          <executions>
            <execution>
              <id>attach-sources</id>
              <goals>
                <goal>jar-no-fork</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-javadoc-plugin</artifactId>
          <version>2.9.1</version>
          <executions>
            <execution>
              <id>attach-javadocs</id>
              <goals>
                <goal>jar</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
    </plugins>
</build>

Y ya, finalmente, pude ejecutar el mágico:

mvn clean deploy

Si todo ha ido bien, el artefacto estará subido a un repositorio que tendremos que promocionar a «Release» para que se sincronice con el repositorio central… Pero al final ya lo tendremos disponible para todo el mundo…

Aquí podéis encontrar lo que acabo de subir: https://search.maven.org/artifact/com.nomorepass/nomorepass/1.0/jar

Páginas estáticas multilingues y con plantilla tiles en struts2

Si, es un título un poco largo, pero he tardado un poco en encontrar una solución a este problema y quiero compartirlo para todos aquellos que os encontréis en la misma tesitura.

Primero, un pequeño resumen de la situación:

  • Aplicación java con struts2+tiles+urlrewrite
  • internacionalización mediante i18n con archivos de properties
  • Necesidad de un número no trivial de páginas «estáticas» con textos largos poco susceptibles de entrar como property.

Los problemas:

  1. No queremos tener una entrada en el tiles.xml por cada página estática pero queremos utilizar las plantillas existentes.
  2. No queremos generar una acción nueva por cada página
  3. No queremos tener que hacer una nueva entrada urlrewrite por cada página
  4. Queremos poder generar páginas de error si no encontramos una página en un idioma determinado.
  5. Queremos poder generar el texto completo en cada idioma como una página html, no como properties.

Si todavía no os habéis hecho una idea, no os preocupeis, es algo normal. Esta situación no se da todos los días.

La solución

O mejor dicho, mi solución…

Voy a crear una única acción que se encargue de determinar el jsp a cargar dentro de la plantilla tiles y voy a modificar una plantilla existente para inyectarle esa nueva página. Además, como el título de la página estará en la plantilla, voy a encargarme de generar el texto en el idioma adecuado. Además, voy a crear un prefijo para el urlrewrite que permita que todo esto quede bonito para google…

Paso a paso:

Modificación del urlrewite.xml:

<rule>
    	<from>^/web/(.*)$</from>
    	<to>/Estaticas.action?pagina=$1.jsp</to>
    </rule>

Con esta regla le decimos que siempre que tengamos una url con la forma /web/mipagina llamaremos a la acción Estaticas y le pasaremos como parámetro mipagina.jsp

Modificacion en struts.xml:

<action name="Estaticas" 
           class="com.yoprogramo.web.action.EstaticasAction">
  <result>/estatica.jsp</result>
</action>

Con esa acción lo que hacemos es llamar a EstaticasAction.java (luego lo vemos) y redirigir a estatica.jsp que tiene este contenido (quitando las cabeceras):

<tiles:definition name="estatica.modif" extends="estatica">
  <tiles:putAttribute name="body" value="${pagina}" />
</tiles:definition>
<tiles:insertDefinition name="estatica.modif" />

Lo que estamos indicando es que se modifique la plantilla con nombre estatica, definida en el tiles.xml y que ponga como atributo body el valor que la acción nos ha devuelto en pagina, de esta manera estaremos utilizando la plantilla definida en el tiles.xml, pero pasándose un jsp distinto. Esta forma de utilizar tiles es lo que se denomina «mutable», para poder utilizarla hay que incluir en el archivo web.xml:

  <context-param>
        <param-name>org.apache.tiles.factory.TilesContainerFactory.MUTABLE</param-name>
        <param-value>true</param-value>
  </context-param>

Ahora que tenemos todo en su sitio, solo tenemos que crear una estructura donde guardar las páginas de cada idioma y poder localizarlas facilmente. Por ejemplo, yo he creado una con esta estructrura:

Arbol de páginas estáticas

Bajo «es» pondré las páginas en español, en «en» las páginas en inglés y en «multi» las páginas multilingues que utilizan el packages.properties para traducir sus claves.

Lo único que nos queda ahora es programar la acción EstaticasAction.java .. Os dejo el código del execute:

		// Eliminamos ruta de lenguaje
		int idx=pagina.lastIndexOf("/");
		String page_name = pagina;
		
		if (idx>0)
			page_name = pagina.substring(idx+1);
		
		idx = page_name.lastIndexOf(".");
		if (idx>0)
			page_name=page_name.substring(0,idx);
		
		//Parte multilingue.
		//Vamos a crear una página para cada idioma, excepto para las
		//que sean muy simples y esas estarán en el directorio /multi
		if (!pagina.contains("/multi")) {
			String lang = getText("locale.language");
			if (lang==null)
				lang="en";
			pagina = "/"+lang+"/"+pagina;
		}
		
		// Ahora comprobamos si existe la página y si no existe redirigimos
		// a una página de error del tipo multi
		String servletContext = 
                     ServletActionContext.getServletContext()
                                                 .getRealPath("/estaticas");
		String filePagina = servletContext+pagina;
		File f = new File (filePagina);
		if (f.exists())
			pagina="/estaticas"+pagina;
		else
			pagina="/estaticas/multi/noexiste.jsp";
		
		// Ahora ponemos el título, que deberá estar como un texto 
		// en package con la forma web.<nombre_pagina>.titulo
		String key = "web."+page_name+".titulo";
		titulo = getText(key);
		
		return SUCCESS;

Una vez todo puesto en su sitio la mecánica para crear páginas estáticas y usarlas en nuestra aplicación es bastante simple:

  1. Crear un jsp con el texto para cada idioma y colocarlos bajo el directorio correcto (p.je. es/mipagina.jsp y en/mipagina.jsp)
  2. Crear una entrada en el package_en.properties y package_es.properties con la clave web.mipagina.titulo indicando el título de la página en cada idioma. (Recordemos que el titulo está en el head de la página y eso suele estar en la plantilla, no en el jsp que estamos modificando).
  3. Ya podemos acceder a /web/mipagina y ver cómo queda dentro de nuestra plantilla.

Igual inicialmente parece mucho trabajo, pero una vez hecho esto podréis hacer tantas páginas estáticas como queráis sin ningún esfuerzo y ligadas a las plantillas del resto de vuestra aplicación.