Skip to content

Commit d8d8dc3

Browse files
committed
Redact query string values
1 parent aa86281 commit d8d8dc3

File tree

2 files changed

+151
-3
lines changed

2 files changed

+151
-3
lines changed

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java

+74-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor;
1818
import io.opentelemetry.semconv.HttpAttributes;
1919
import io.opentelemetry.semconv.UrlAttributes;
20+
import java.util.Arrays;
21+
import java.util.HashSet;
22+
import java.util.Set;
2023
import java.util.function.ToIntFunction;
2124
import javax.annotation.Nullable;
2225

@@ -32,6 +35,9 @@ public final class HttpClientAttributesExtractor<REQUEST, RESPONSE>
3235
REQUEST, RESPONSE, HttpClientAttributesGetter<REQUEST, RESPONSE>>
3336
implements SpanKeyProvider {
3437

38+
private static final Set<String> PARAMS_TO_REDACT =
39+
new HashSet(Arrays.asList("AWSAccessKeyId", "Signature", "sig", "X-Goog-Signature"));
40+
3541
/**
3642
* Creates the HTTP client attributes extractor with default configuration.
3743
*
@@ -54,6 +60,7 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST,
5460
private final InternalNetworkAttributesExtractor<REQUEST, RESPONSE> internalNetworkExtractor;
5561
private final InternalServerAttributesExtractor<REQUEST> internalServerExtractor;
5662
private final ToIntFunction<Context> resendCountIncrementer;
63+
private final boolean redactSensitiveParameters;
5764

5865
HttpClientAttributesExtractor(HttpClientAttributesExtractorBuilder<REQUEST, RESPONSE> builder) {
5966
super(
@@ -65,6 +72,9 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST,
6572
internalNetworkExtractor = builder.buildNetworkExtractor();
6673
internalServerExtractor = builder.buildServerExtractor();
6774
resendCountIncrementer = builder.resendCountIncrementer;
75+
redactSensitiveParameters =
76+
Boolean.getBoolean(
77+
"otel.instrumentation.http.client.experimental.redact-sensitive-parameters");
6878
}
6979

7080
@Override
@@ -104,7 +114,7 @@ public SpanKey internalGetSpanKey() {
104114
}
105115

106116
@Nullable
107-
private static String stripSensitiveData(@Nullable String url) {
117+
private String stripSensitiveData(@Nullable String url) {
108118
if (url == null || url.isEmpty()) {
109119
return url;
110120
}
@@ -141,8 +151,69 @@ private static String stripSensitiveData(@Nullable String url) {
141151
}
142152

143153
if (atIndex == -1 || atIndex == len - 1) {
144-
return url;
154+
return redactSensitiveParameters ? redactUrlParameters(url) : url;
155+
}
156+
157+
String afterUserPwdRedaction = url.substring(atIndex);
158+
return url.substring(0, schemeEndIndex + 3)
159+
+ "REDACTED:REDACTED"
160+
+ (redactSensitiveParameters
161+
? redactUrlParameters(afterUserPwdRedaction)
162+
: afterUserPwdRedaction);
163+
}
164+
165+
private static String redactUrlParameters(String urlpart) {
166+
167+
int questionMarkIndex = urlpart.indexOf('?');
168+
169+
if (questionMarkIndex == -1) {
170+
return urlpart;
171+
}
172+
173+
if (!containsParamToRedact(urlpart)) {
174+
return urlpart;
175+
}
176+
177+
StringBuilder redactedParameters = new StringBuilder();
178+
boolean paramToRedact = false;
179+
boolean paramNameDetected = false;
180+
boolean reference = false;
181+
182+
StringBuilder currentParamName = new StringBuilder();
183+
184+
for (int i = questionMarkIndex + 1; i < urlpart.length(); i++) {
185+
char currentChar = urlpart.charAt(i);
186+
if (currentChar == '=') {
187+
paramNameDetected = true;
188+
redactedParameters.append(currentParamName);
189+
redactedParameters.append('=');
190+
if (PARAMS_TO_REDACT.contains(currentParamName.toString())) {
191+
redactedParameters.append("REDACTED");
192+
paramToRedact = true;
193+
}
194+
} else if (currentChar == '&') {
195+
redactedParameters.append('&');
196+
paramNameDetected = false;
197+
paramToRedact = false;
198+
currentParamName.setLength(0);
199+
} else if (currentChar == '#') {
200+
reference = true;
201+
redactedParameters.append('#');
202+
} else if (!paramNameDetected) {
203+
currentParamName.append(currentChar);
204+
} else if (!paramToRedact || reference) {
205+
redactedParameters.append(currentChar);
206+
}
207+
}
208+
return urlpart.substring(0, questionMarkIndex) + "?" + redactedParameters;
209+
}
210+
211+
private static boolean containsParamToRedact(String urlpart) {
212+
for (String param : PARAMS_TO_REDACT) {
213+
if (urlpart.contains(param)) {
214+
return true;
215+
}
145216
}
146-
return url.substring(0, schemeEndIndex + 3) + "REDACTED:REDACTED" + url.substring(atIndex);
217+
return false;
147218
}
148219
}

instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java

+77
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static java.util.Collections.emptyMap;
2424
import static java.util.Collections.singletonList;
2525
import static org.assertj.core.api.Assertions.entry;
26+
import static org.junit.jupiter.params.provider.Arguments.arguments;
2627

2728
import io.opentelemetry.api.common.AttributeKey;
2829
import io.opentelemetry.api.common.Attributes;
@@ -36,9 +37,13 @@
3637
import java.util.List;
3738
import java.util.Map;
3839
import java.util.function.ToIntFunction;
40+
import java.util.stream.Stream;
3941
import javax.annotation.Nullable;
4042
import org.junit.jupiter.api.Test;
43+
import org.junit.jupiter.api.extension.ExtensionContext;
4144
import org.junit.jupiter.params.ParameterizedTest;
45+
import org.junit.jupiter.params.provider.Arguments;
46+
import org.junit.jupiter.params.provider.ArgumentsProvider;
4247
import org.junit.jupiter.params.provider.ArgumentsSource;
4348
import org.junit.jupiter.params.provider.ValueSource;
4449

@@ -200,6 +205,78 @@ void normal() {
200205
entry(NETWORK_PEER_PORT, 456L));
201206
}
202207

208+
@ParameterizedTest
209+
@ArgumentsSource(StripUrlArgumentSource.class)
210+
void stripBasicAuthTest(String url, String expectedResult) {
211+
Map<String, String> request = new HashMap<>();
212+
request.put("urlFull", url);
213+
214+
System.setProperty(
215+
"otel.instrumentation.http.client.experimental.redact-sensitive-parameters", "true");
216+
AttributesExtractor<Map<String, String>, Map<String, String>> extractor =
217+
HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter());
218+
219+
AttributesBuilder attributes = Attributes.builder();
220+
extractor.onStart(attributes, Context.root(), request);
221+
222+
assertThat(attributes.build()).containsOnly(entry(URL_FULL, expectedResult));
223+
}
224+
225+
static final class StripUrlArgumentSource implements ArgumentsProvider {
226+
227+
@Override
228+
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
229+
return Stream.of(
230+
arguments("https://user1:[email protected]", "https://REDACTED:[email protected]"),
231+
arguments(
232+
"https://user1:[email protected]/path/",
233+
"https://REDACTED:[email protected]/path/"),
234+
arguments(
235+
"https://user1:[email protected]#test.html",
236+
"https://REDACTED:[email protected]#test.html"),
237+
arguments(
238+
"https://user1:[email protected]?foo=b@r",
239+
"https://REDACTED:[email protected]?foo=b@r"),
240+
arguments(
241+
"https://user1:[email protected]/p@th?foo=b@r",
242+
"https://REDACTED:[email protected]/p@th?foo=b@r"),
243+
arguments("https://github.com/p@th?foo=b@r", "https://github.com/p@th?foo=b@r"),
244+
arguments("https://github.com#[email protected]", "https://github.com#[email protected]"),
245+
arguments("user1:[email protected]", "user1:[email protected]"),
246+
arguments("https://github.com@", "https://github.com@"),
247+
arguments(
248+
"https://service.com?paramA=valA&paramB=valB",
249+
"https://service.com?paramA=valA&paramB=valB"),
250+
arguments(
251+
"https://service.com?AWSAccessKeyId=AKIAIOSFODNN7",
252+
"https://service.com?AWSAccessKeyId=REDACTED"),
253+
arguments(
254+
"https://service.com?Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0%3A377",
255+
"https://service.com?Signature=REDACTED"),
256+
arguments(
257+
"https://service.com?sig=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0",
258+
"https://service.com?sig=REDACTED"),
259+
arguments(
260+
"https://service.com?X-Goog-Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0",
261+
"https://service.com?X-Goog-Signature=REDACTED"),
262+
arguments(
263+
"https://service.com?paramA=valA&AWSAccessKeyId=AKIAIOSFODNN7&paramB=valB",
264+
"https://service.com?paramA=valA&AWSAccessKeyId=REDACTED&paramB=valB"),
265+
arguments(
266+
"https://service.com?AWSAccessKeyId=AKIAIOSFODNN7&paramA=valA",
267+
"https://service.com?AWSAccessKeyId=REDACTED&paramA=valA"),
268+
arguments(
269+
"https://service.com?paramA=valA&AWSAccessKeyId=AKIAIOSFODNN7",
270+
"https://service.com?paramA=valA&AWSAccessKeyId=REDACTED"),
271+
arguments(
272+
"https://service.com?AWSAccessKeyId=AKIAIOSFODNN7&AWSAccessKeyId=ZGIAIOSFODNN7",
273+
"https://service.com?AWSAccessKeyId=REDACTED&AWSAccessKeyId=REDACTED"),
274+
arguments(
275+
"https://service.com?AWSAccessKeyId=AKIAIOSFODNN7#ref",
276+
"https://service.com?AWSAccessKeyId=REDACTED#ref"));
277+
}
278+
}
279+
203280
@ParameterizedTest
204281
@ArgumentsSource(ValidRequestMethodsProvider.class)
205282
void shouldExtractKnownMethods(String requestMethod) {

0 commit comments

Comments
 (0)