Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2ba9494
feat(scrubbing): add StringUrlSanitizer and DefaultUrlSanitizer
buongarzoni Jul 13, 2026
9049736
feat(scrubbing): add ScrubDataTransformer
buongarzoni Jul 13, 2026
7a9ca73
feat(config): expose redactedKeys and urlSanitizer
buongarzoni Jul 13, 2026
00dc4cb
feat(notifier): apply built-in scrubbing to every payload
buongarzoni Jul 13, 2026
bf3629d
refactor(okhttp): reuse the shared DefaultUrlSanitizer
buongarzoni Jul 13, 2026
9fee327
refactor(api): update imports
buongarzoni Jul 13, 2026
347ad48
refactor(scrubbing): update imports
buongarzoni Jul 13, 2026
306b105
refactor(config): update imports
buongarzoni Jul 13, 2026
52d5d40
refactor(okhttp): reuse the shared DefaultUrlSanitizer
buongarzoni Jul 13, 2026
82fb70b
fix(scrubbing): scrub Frame.locals in Body.rollbarThreads
buongarzoni Jul 13, 2026
8c06fd1
fix(scrubbing): match percent-encoded query parameter names
buongarzoni Jul 13, 2026
a09f485
fix(scrubbing): scrub Request.params and Request.metadata
buongarzoni Jul 13, 2026
57399ec
fix(scrubbing): traverse collections and arrays when scrubbing nested…
buongarzoni Aug 3, 2026
5482e72
fix(telemetry): sanitize URLs recorded as network telemetry events
buongarzoni Aug 3, 2026
f7bc6f3
test(scrubbing): cover ordering, reconfiguration and the okhttp sanit…
buongarzoni Aug 3, 2026
4c21b07
docs: add scrubbing documentation
buongarzoni Aug 10, 2026
1fc3ed6
feat(scrubbing): seed field scrubbing with a built-in key list
buongarzoni Aug 11, 2026
f016ac2
feat(config): expose useDefaultRedactedKeys
buongarzoni Aug 11, 2026
9ee5bc7
test(scrubbing): cover the built-in key list and the opt-out
buongarzoni Aug 11, 2026
f357b6c
test(scrubbing): prove secrets are redacted with no configuration
buongarzoni Aug 11, 2026
2ede510
docs: document the built-in redacted key list
buongarzoni Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ For actual usage, the easiest way to get started is by looking at the examples:
- [rollbar-spring-boot-webmvc](https://github.com/rollbar/rollbar-java/tree/master/examples/rollbar-spring-boot-webmvc)
- [rollbar-reactive-streams-reactor](https://github.com/rollbar/rollbar-java/tree/master/examples/rollbar-reactive-streams-reactor)

## Data scrubbing

Payloads are scrubbed before they are sent, with no configuration required: fields named after
secrets (`password`, `secret`, `token`, `authorization`, `api_key`, …) and a deny-list of
authentication headers are redacted, and URLs have their userinfo, query string and fragment
stripped. You can add your own keys with `redactedKeys`, turn the built-in key list off with
`useDefaultRedactedKeys(false)`, and change the URL handling with `urlSanitizer`.

See [SCRUBBING.md](SCRUBBING.md) for what is redacted by default, how to configure it, and the
migration impact if you are upgrading from a version minor or equal than 2.3.1.

## Release History & Changelog

See our [Releases](https://github.com/rollbar/rollbar-java/releases) page for a list of all releases, including changes.
Expand Down
118 changes: 118 additions & 0 deletions SCRUBBING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Data scrubbing

Every occurrence the notifier builds — anything reported through `log`, `debug`, `info`,
`warning`, `error` or `critical`, including uncaught exceptions — is passed through a built-in
scrubber before it is sent. It runs **after** any `Transformer` you configure, so a transformer
cannot be used to opt out of it.

This applies to all three notifiers, since they share the same configuration and send path:

| Module | Covered |
| --- | --- |
| `rollbar-java` | yes |
| `rollbar-reactive-streams` | yes |
| `rollbar-android` | yes |

The exception is `Rollbar.sendJsonPayload(String)`, which hands an already-serialized payload
straight to the sender and skips transformers, filters and scrubbing alike. Nothing on this page
applies to it; scrub that JSON yourself before passing it in.

## What is redacted without any configuration

- **Fields whose key names a secret**, wherever they appear in the payload. The built-in list is

| Pattern | Also matches |
| --- | --- |
| `password` | `user_password`, `passwordConfirmation`, `passwordHash` |
| `passwd` | |
| `secret` | `client_secret`, `secretKey` |
| `token` | `access_token`, `auth_token`, `csrfToken`, `refreshToken` |
| `authorization` | `proxy_authorization` |
| `authentication` | |
| `^auth$` | anchored on purpose, so `author` is left alone |
| `api[-_]?key` | `api_key`, `apiKey`, `API-KEY` |

Matching is case-insensitive and, apart from `^auth$`, matches anywhere in the key. So
`GET /login?password=hunter2` arrives with `request.get.password`, `request.query_string` and
the `request.url` query all redacted.
- **Request headers**, matched case-insensitively against a built-in deny-list:
`Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`, `X-Auth-Token`, `X-Access-Token`,
`X-Secret`, `Proxy-Authorization`, `WWW-Authenticate`. The value becomes `***`.
- **URLs**, which have their userinfo, query string and fragment stripped. This covers
`request.url` and the URLs recorded by `Rollbar.recordNetworkEventFor(...)`, so
`https://user:pass@example.com/orders?token=secret` is reported as
`https://example.com/orders`.

Not covered: `request.body`, which is a raw string the notifier cannot parse. If you populate it,
scrub it yourself.

## Redacting your own keys

`redactedKeys` takes a list of **case-insensitive regexes**, added to the built-in list above. A
key is redacted when the regex is found anywhere in it, so `"pin"` also matches `pin_code`.

```java
Config config = ConfigBuilder.withAccessToken(ACCESS_TOKEN)
.redactedKeys(Arrays.asList("ssn", "pin", "date_of_birth"))
.build();
```

They are matched against the keys of: request headers, routing parameters (`request.params`),
GET and POST parameters, `request.metadata`, the raw `request.query_string`, custom data, and
`Frame.locals` — including the copies carried by `body.threads` when JVMTI locals capture is
enabled. Matching values are replaced with `***`.

To match only your own keys, turn the built-in list off. The header deny-list and the URL
sanitizer still apply:

```java
Config config = ConfigBuilder.withAccessToken(ACCESS_TOKEN)
.redactedKeys(Arrays.asList("ssn"))
.useDefaultRedactedKeys(false)
.build();
```

Nested data is walked recursively through maps, collections and arrays, up to 8 levels of
nesting, and the surrounding shape is preserved. Given `redactedKeys(["password"])`:

```java
rollbar.error(exception, Collections.singletonMap(
"users", Arrays.asList(Collections.singletonMap("password", "hunter2"))));
// sent as: {"users": [{"password": "***"}]}
```

When a key itself matches, its whole value is replaced rather than descended into.

## Customizing URL sanitization

Supply a `StringUrlSanitizer` to change or disable the URL handling:

```java
Config config = ConfigBuilder.withAccessToken(ACCESS_TOKEN)
.urlSanitizer(url -> url) // keep URLs verbatim
.build();
```

If you use the OkHttp interceptor, share the same sanitizer so both paths redact identically:

```java
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(RollbarOkHttpInterceptor.withSharedUrlSanitizer(
recorder, config.urlSanitizer()))
.build();
```

See the [rollbar-okhttp README](rollbar-okhttp/README.md) for the interceptor's own sanitizer
options.

## Migrating

This is a behaviour change: no configuration is required to get the redaction above, and it
cannot be disabled from a `Transformer`. If you are upgrading, expect that

- values matching the built-in key list, the header deny-list or your `redactedKeys` now arrive
as `***` — including keys you may not consider sensitive, such as `tokenCount`. Set
`useDefaultRedactedKeys(false)` if the built-in list is too broad for your payloads;
- `request.url` and network telemetry URLs no longer carry credentials, query strings or
fragments. If you rely on query parameters for grouping or search, configure a
`urlSanitizer` that preserves them.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.rollbar.api.scrubbing;

/**
* Default {@link StringUrlSanitizer} that strips userinfo, query string, and fragment from URLs.
* Uses string scanning rather than {@code java.net.URI} to avoid allocation on clean URLs
* and to preserve the original percent-encoding without normalization.
*/
public final class DefaultUrlSanitizer implements StringUrlSanitizer {

public static final DefaultUrlSanitizer INSTANCE = new DefaultUrlSanitizer();

private DefaultUrlSanitizer() {
}

@Override
public String sanitize(String url) {
if (url == null) {
return null;
}
// Fast path: no characters that can introduce query string, fragment, or userinfo.
if (url.indexOf('?') < 0 && url.indexOf('#') < 0 && url.indexOf('@') < 0) {
return url;
}
return strip(url);
}

private static String strip(String url) {
int end = url.length();
int q = url.indexOf('?');
int f = url.indexOf('#');
if (q >= 0 && q < end) {
end = q;
}
if (f >= 0 && f < end) {
end = f;
}
// Strip userinfo: find "://" then the last "@" before the first "/" after the authority start.
String result = url.substring(0, end);
int schemeEnd = result.indexOf("://");
if (schemeEnd >= 0) {
int hostStart = schemeEnd + 3;
int slashAfterHost = result.indexOf('/', hostStart);
int searchEnd = slashAfterHost < 0 ? result.length() : slashAfterHost;
int at = result.lastIndexOf('@', searchEnd);
if (at >= hostStart) {
result = result.substring(0, hostStart) + result.substring(at + 1);
}
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.rollbar.api.scrubbing;

/**
* Sanitizes a URL string before it is included in a Rollbar payload.
* Implementations should strip sensitive components such as userinfo,
* query parameters, and fragments.
*/
@FunctionalInterface
public interface StringUrlSanitizer {
/**
* Returns a sanitized version of the given URL string, or {@code null} if
* the input is {@code null}.
*
* @param url the raw URL string, may be {@code null}.
* @return the sanitized URL, or {@code null}.
*/
String sanitize(String url);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.rollbar.api.scrubbing;

import org.junit.Test;

import static org.junit.Assert.*;

public class DefaultUrlSanitizerTest {

private final DefaultUrlSanitizer sanitizer = DefaultUrlSanitizer.INSTANCE;

@Test
public void nullInputReturnsNull() {
assertNull(sanitizer.sanitize(null));
}

@Test
public void cleanUrlUnchanged() {
String url = "https://example.com/api/v1/things";
assertEquals(url, sanitizer.sanitize(url));
}

@Test
public void queryStringStripped() {
assertEquals(
"https://example.com/search",
sanitizer.sanitize("https://example.com/search?token=abc&page=1")
);
}

@Test
public void fragmentStripped() {
assertEquals(
"https://example.com/page",
sanitizer.sanitize("https://example.com/page#section")
);
}

@Test
public void userinfoStripped() {
assertEquals(
"https://example.com/path",
sanitizer.sanitize("https://user:pass@example.com/path")
);
}

@Test
public void allThreeScrubbed() {
assertEquals(
"https://example.com/path",
sanitizer.sanitize("https://admin:secret@example.com/path?token=xyz#top")
);
}

@Test
public void malformedUrlNoException() {
// Should not throw; best-effort strip
String result = sanitizer.sanitize("not-a-url?query=sensitive");
assertNotNull(result);
assertFalse(result.contains("sensitive"));
}

@Test
public void malformedUrlWithUserinfo() {
String result = sanitizer.sanitize("http://user:secret@host/path?q=1");
assertNotNull(result);
assertFalse(result.contains("secret"));
assertFalse(result.contains("q=1"));
}

@Test
public void emptyStringUnchanged() {
assertEquals("", sanitizer.sanitize(""));
}

@Test
public void cleanUrlReturnedAsSameInstance() {
String url = "https://example.com/api/v1/things";
assertSame(url, sanitizer.sanitize(url));
}

@Test
public void percentEncodedPathPreserved() {
// No ?, #, or @ — fast path must return the same instance without normalizing encoding.
String url = "https://example.com/path%20with%20spaces";
assertSame(url, sanitizer.sanitize(url));
}

@Test
public void atSignInPathNotTreatedAsUserinfo() {
// The @ is after the first path slash, so it is not userinfo.
String url = "https://example.com/users/@alice?token=x";
String result = sanitizer.sanitize(url);
assertTrue(result.contains("@alice"));
assertFalse(result.contains("token"));
}
}
Loading
Loading