Ofuscar datos sensibles en las trazas con Log4j

Escrito por el .
java planeta-codigo programacion
Comentarios

Java

Los archivos de trazas o logs contienen información de lo que ha realizado la aplicación. Estos registros de información contienen los datos que el desarrollador considera de utilidad en caso de necesitar su consulta. Algunos datos son especialmente sensibles ya que su obtención permiten acceder a cuentas de usuario, obtener datos como tarjetas de crédito o cuentas bancarias, contraseñas o bearer tokens de peticiones HTTP que autorizan el acceso. Proteger las contraseñas hasheandolas aún con salt y cifrar información por motivos seguridad y privacidad es inútil si luego esta información está presente en los archivos de log en texto plano.

Log4j es una de las librerías más utilizadas para añadir la funcionalidad de las trazas en una aplicación Java. Proteger algunos datos sensibles se puede hacer de varias formas. Una de ellas es hacer que sea la aplicación la que se encargue de no emitir estos datos en las trazas u ofuscarla enmascarándola al toda o parte. Para este caso se pueden utilizar objetos Message que adaptan los objetos de la aplicación a los datos a emitir en las trazas pero requiere modificar en todos los puntos de la aplicación.

En el siguiente ejemplo se hace uso de lookahead como se detalla en la clase Pattern de Java para añadir la funcionalidad de que los últimos caracteres queden visibles y la clase SecuredMessage aplica expresiones regulares al mensaje, en caso de encontrar una coincidencia realiza la ofuscación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.blogbitix.log4j;

...

public class Main {

    private static final Logger logger = LogManager.getLogger(Main.class);

    public static void main(String[] args) {
        ...

        logger.info(new SecuredMessage("Tarjeta de crédito: 1111 1111 1111 1111, DNI: 11111111A", Arrays.asList("(\\d{4} \\d{4} \\d{4} \\d{1})(?=\\d{3})", "(\\d{6})(?=\\d{2}[A-Z])")));
        logger.info("Tarjeta de crédito: 1111 1111 1111 1111, DNI: 11111111A");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package io.github.picodotdev.blogbitix.log4j;

import org.apache.logging.log4j.message.Message;

import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class SecuredMessage implements Message {

    private static final int UNMASKED_CHARACTERS = 3;

    private Message message;
    private String string;
    private Pattern pattern;

    public SecuredMessage(Message message, Collection<String> patterns) {
        this.message = message;
        this.pattern = compilePatterns(patterns);
    }

    public SecuredMessage(String string, Collection<String> patterns) {
        this.string = string;
        this.pattern = compilePatterns(patterns);
    }

    ...

    @Override
    public String getFormattedMessage() {
        return securedMessage();
    }

    private String securedMessage() {
        if (message != null) {
            return securedMessage(message);
        } else if (string != null) {
            return securedString(string);
        }
        return "";
    }

    private Pattern compilePatterns(Collection<String> patterns) {
        return Pattern.compile(patterns.stream().map(it -> "(" + it + ")").collect(Collectors.joining("|")));
    }

    private String securedMessage(Message message) {
        return securedString(message.getFormattedMessage());
    }

    private String securedString(String string) {
        String result = string;
        Matcher matcher = pattern.matcher(string);
        while (matcher.find()) {
            String match = matcher.group();
            String mask = mask(match);
            result = result.replaceFirst(match, mask);
        }
        return result;
    }

    private String mask(String string) {
        return string.replaceAll(".", "*");
    }
}

Utilizar una clase que implemente la interfaz Message para realizar el reemplazo requiere modificar todos los puntos de la aplicación que emitan información sensible, para evitar posibles omisiones este aspecto de la aplicación se puede delegar en Log4j y ser aplicado de forma global.

Con los parámetros de configuración replace, regex y replacement el reemplazo los hace la clase PatterLayout utilizando una expresión similar regular que en el caso de SecuredMessage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
configuration:
  status: warn

  appenders:
    console:
      name: STDOUT
      patternLayout:
        pattern: "%d{DEFAULT} %X{uuid} %-5level %60.60logger %msg%n"
        replace: 
          regex: "(\\d{4} \\d{4} \\d{4} \\d{1})(?=\\d{3})|(\\d{6})(?=\\d{2}[A-Z])"
          replacement: "**********"

  loggers:
    root:
      level: info
      appenderRef:
        ref: STDOUT

En la salida del ejemplo la primera traza corresponde al uso de la clase SecurdMessage y la segunda al PatternLayout.

1
2
3
...
2019-02-10 11:22:47,652  INFO                     io.github.picodotdev.blogbitix.log4j.Main Tarjeta de crédito: ****************111, DNI: ******11A
2019-02-10 11:22:47,653  INFO                     io.github.picodotdev.blogbitix.log4j.Main Tarjeta de crédito: **********111, DNI: **********11A

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.