From e96202b3c0da0bcec283f13de5be9f4ddb1f962c Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 13 Nov 2026 02:53:57 -0300 Subject: [PATCH] fix(api): mask access token in Payload.toString() --- .../java/com/rollbar/api/payload/Payload.java | 21 ++++++++++- .../com/rollbar/api/payload/PayloadTest.java | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/rollbar-api/src/main/java/com/rollbar/api/payload/Payload.java b/rollbar-api/src/main/java/com/rollbar/api/payload/Payload.java index 842e0963..c8e3c291 100755 --- a/rollbar-api/src/main/java/com/rollbar/api/payload/Payload.java +++ b/rollbar-api/src/main/java/com/rollbar/api/payload/Payload.java @@ -16,6 +16,10 @@ public class Payload implements JsonSerializable, StringTruncatable { private static final long serialVersionUID = 3054041443735286159L; + private static final String TOKEN_MASK = "****"; + + private static final int UNMASKED_TOKEN_SUFFIX_LENGTH = 4; + private final String accessToken; private final Data data; @@ -86,11 +90,26 @@ public int hashCode() { @Override public String toString() { return "Payload{" - + "accessToken='" + accessToken + '\'' + + "accessToken=" + maskAccessToken(accessToken) + ", data=" + data + '}'; } + /** + * Masks the token so it never ends up in logs. Keeps the last characters of long enough tokens + * so a misconfigured token can still be told apart from the right one. + */ + private static String maskAccessToken(String accessToken) { + if (accessToken == null) { + return "null"; + } + if (accessToken.length() <= UNMASKED_TOKEN_SUFFIX_LENGTH * 2) { + return "'" + TOKEN_MASK + "'"; + } + return "'" + TOKEN_MASK + + accessToken.substring(accessToken.length() - UNMASKED_TOKEN_SUFFIX_LENGTH) + "'"; + } + @Override public Map asJson() { Map values = new HashMap<>(); diff --git a/rollbar-api/src/test/java/com/rollbar/api/payload/PayloadTest.java b/rollbar-api/src/test/java/com/rollbar/api/payload/PayloadTest.java index 868a0afb..e45dab32 100644 --- a/rollbar-api/src/test/java/com/rollbar/api/payload/PayloadTest.java +++ b/rollbar-api/src/test/java/com/rollbar/api/payload/PayloadTest.java @@ -1,6 +1,8 @@ package com.rollbar.api.payload; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import com.rollbar.api.payload.data.Data; import java.util.HashMap; @@ -53,6 +55,40 @@ public void shouldReturnAsJson() { assertEquals(expected, payload.asJson()); } + @Test + public void toStringShouldNotContainAccessToken() { + Payload payload = new Payload.Builder() + .accessToken(ACCESS_TOKEN) + .data(DATA) + .build(); + + String result = payload.toString(); + + assertFalse(result.contains(ACCESS_TOKEN)); + assertTrue(result.contains("accessToken='****ddsf'")); + } + + @Test + public void toStringShouldFullyMaskShortAccessToken() { + Payload payload = new Payload.Builder() + .accessToken("abcd1234") + .build(); + + String result = payload.toString(); + + assertFalse(result.contains("1234")); + assertTrue(result.contains("accessToken='****'")); + } + + @Test + public void toStringShouldHandleNullAccessToken() { + Payload payload = new Payload.Builder() + .data(DATA) + .build(); + + assertTrue(payload.toString().contains("accessToken=null")); + } + @Test public void shouldReturnJsonAsJson() { String json = "{\"foo\":\"bar\"}";