Mask sensitive data with custom Jackson annotations
2018-06-29 / modified at 2025-01-29 / 388 words / 2 mins

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

Here is a User, in which the tel field contains a customer’s privacy information that may not be allowed to persist on the server under certain regulations (such as GDPR).

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 is printed, which is not allowed under GDPR

Solution

Step 1: 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 "***";
}

Step 2: Wrap the field with a annotation.

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

Step 3: Create a custom 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);
}
}

Updated Test case:

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 safely log the user data without exposing sensitive information.

Other Recommendations

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

  • SQL: Be careful while logging SQL queries with frameworks like Mybatis. For example, select user from users where email = ? may leak parameters.
  • HTTP Header: Avoid printing sensitive headers (e.g., authentication tokens, X-Forward-For) in logs, especially when using interceptors like OkHttp.
  • Socket: Validate payload before logging readline() output.
  • Third-party crash SDKs: Restrict outbound traffic when possible, as it’s hard to prevent data smuggling.