Mask sensitive data with custom jackson annotations
2018-06-29 / modified at 2023-10-29 / 372 words / 2 mins

In this article, we’ll see how to use custom jackson annotations to mask sensitive data with asterisk.

Here is a user, in which the tel field contains a customer’s privacy, that may not be allowed to persist on the server under some laws (GDPR or else).

1
2
3
4
5
public class User {
String nick;
String tel;
//getter/setter...
}

Test case

1
2
3
4
5
6
User user = new User();
user.setNick("suzumiya");
user.setTel("08012345");
String s = new ObjectMapper().writeValueAsString(user);
assert s.equals("{\"nick\":\"suzumiya\",\"tel\":\"08012345\"}");
// the telephone number was printed, which is not allowed under GDPR

Solution

First, create an annotation

1
2
3
4
5
6
7
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = AsteriskSerializer.class)
public @interface Asterisk {
String value() default "***";
}

Wrap the field with annotation.

1
2
3
4
5
6
public class User {
String name;
+ @Asterisk()
String password;
//getter/setter...
}

Create a customer serializer

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
public class AsteriskSerializer extends StdSerializer<Object> implements ContextualSerializer {

String asterisk;

public AsteriskSerializer() {
super(Object.class);
}

public AsteriskSerializer(String asterisk) {
super(Object.class);
this.asterisk = asterisk;
}

@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty property) {
Optional<AsteriskOnCondition> anno = Optional.ofNullable(property)
.map(prop -> prop.getAnnotation(Asterisk.class));
return new AsteriskSerializer(anno.map(Asterisk::value).orElse(null));
}

@Override
public void serialize(Object obj, JsonGenerator gen, SerializerProvider prov) throws IOException {
gen.writeString(asterisk);
}
}

Let’s use these in test again

1
2
3
4
5
User user = new User();
user.setNick("suzumiya");
user.setTel("08012345");
String s = new ObjectMapper().writeValueAsString(user);
assert s.equals("{\"nick\":\"suzumiya\",\"tel\":\"***\"}");

Now, you can log the user safely.

Other hints

Here are some other potential leaks of which should be taken care

  • SQL: Be careful while logging with mybatis, eg select user from users where token = ? may leak the first parameter.
  • HTTP Header: Don’t print token/key/auth headers, such as okhttp interceptors.
  • Socket: Check your readline() payload before logging.
  • Third party crash SDKs: It’s not recommend for allowing outbound traffics as it’s hard to prevent data smuggling.