Skip to content

Commit 608bc78

Browse files
committed
Redact query string values
1 parent aa86281 commit 608bc78

File tree

2 files changed

+150
-3
lines changed

2 files changed

+150
-3
lines changed

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

+73-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
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.List;
2022
import java.util.function.ToIntFunction;
2123
import javax.annotation.Nullable;
2224

@@ -32,6 +34,9 @@ public final class HttpClientAttributesExtractor<REQUEST, RESPONSE>
3234
REQUEST, RESPONSE, HttpClientAttributesGetter<REQUEST, RESPONSE>>
3335
implements SpanKeyProvider {
3436

37+
private static final List<String> PARAMS_TO_REDACT =
38+
Arrays.asList("AWSAccessKeyId", "Signature", "sig", "X-Goog-Signature");
39+
3540
/**
3641
* Creates the HTTP client attributes extractor with default configuration.
3742
*
@@ -54,6 +59,7 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST,
5459
private final InternalNetworkAttributesExtractor<REQUEST, RESPONSE> internalNetworkExtractor;
5560
private final InternalServerAttributesExtractor<REQUEST> internalServerExtractor;
5661
private final ToIntFunction<Context> resendCountIncrementer;
62+
private final boolean redactSensitiveParameters;
5763

5864
HttpClientAttributesExtractor(HttpClientAttributesExtractorBuilder<REQUEST, RESPONSE> builder) {
5965
super(
@@ -65,6 +71,9 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST,
6571
internalNetworkExtractor = builder.buildNetworkExtractor();
6672
internalServerExtractor = builder.buildServerExtractor();
6773
resendCountIncrementer = builder.resendCountIncrementer;
74+
redactSensitiveParameters =
75+
Boolean.getBoolean(
76+
"otel.instrumentation.http.client.experimental.redact-sensitive-parameters");
6877
}
6978

7079
@Override
@@ -104,7 +113,7 @@ public SpanKey internalGetSpanKey() {
104113
}
105114

106115
@Nullable
107-
private static String stripSensitiveData(@Nullable String url) {
116+
private String stripSensitiveData(@Nullable String url) {
108117
if (url == null || url.isEmpty()) {
109118
return url;
110119
}
@@ -141,8 +150,69 @@ private static String stripSensitiveData(@Nullable String url) {
141150
}
142151

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

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)