Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ 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: a deny-list of
authentication headers is redacted, and URLs have their userinfo, query string and fragment
stripped. You can add your own keys with `redactedKeys` 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.

## Release History & Changelog

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

- **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`.

## Redacting your own keys

`redactedKeys` takes a list of **case-insensitive regexes**. A key is redacted when the regex is
found anywhere in it, so `"password"` also matches `user_password`.

```java
Config config = ConfigBuilder.withAccessToken(ACCESS_TOKEN)
.redactedKeys(Arrays.asList("password", "secret", "ssn"))
.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 `***`.

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 header deny-list or your `redactedKeys` now arrive as `***`;
- `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