diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java index 5769d1169c3f..c84e6cd982e9 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java @@ -17,6 +17,8 @@ import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor; import io.opentelemetry.semconv.HttpAttributes; import io.opentelemetry.semconv.UrlAttributes; +import java.util.Arrays; +import java.util.List; import java.util.function.ToIntFunction; import javax.annotation.Nullable; @@ -32,6 +34,9 @@ public final class HttpClientAttributesExtractor<REQUEST, RESPONSE> REQUEST, RESPONSE, HttpClientAttributesGetter<REQUEST, RESPONSE>> implements SpanKeyProvider { + private static final List<String> PARAMS_TO_REDACT = + Arrays.asList("AWSAccessKeyId", "Signature", "sig", "X-Goog-Signature"); + /** * Creates the HTTP client attributes extractor with default configuration. * @@ -54,6 +59,7 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST, private final InternalNetworkAttributesExtractor<REQUEST, RESPONSE> internalNetworkExtractor; private final InternalServerAttributesExtractor<REQUEST> internalServerExtractor; private final ToIntFunction<Context> resendCountIncrementer; + private final boolean redactSensitiveParameters; HttpClientAttributesExtractor(HttpClientAttributesExtractorBuilder<REQUEST, RESPONSE> builder) { super( @@ -65,6 +71,9 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST, internalNetworkExtractor = builder.buildNetworkExtractor(); internalServerExtractor = builder.buildServerExtractor(); resendCountIncrementer = builder.resendCountIncrementer; + redactSensitiveParameters = + Boolean.getBoolean( + "otel.instrumentation.http.client.experimental.redact-sensitive-parameters"); } @Override @@ -104,7 +113,7 @@ public SpanKey internalGetSpanKey() { } @Nullable - private static String stripSensitiveData(@Nullable String url) { + private String stripSensitiveData(@Nullable String url) { if (url == null || url.isEmpty()) { return url; } @@ -141,8 +150,69 @@ private static String stripSensitiveData(@Nullable String url) { } if (atIndex == -1 || atIndex == len - 1) { - return url; + return redactSensitiveParameters ? redactUrlParameters(url) : url; + } + + String afterUserPwdRedaction = url.substring(atIndex); + return url.substring(0, schemeEndIndex + 3) + + "REDACTED:REDACTED" + + (redactSensitiveParameters + ? redactUrlParameters(afterUserPwdRedaction) + : afterUserPwdRedaction); + } + + public static String redactUrlParameters(String urlpart) { + + int questionMarkIndex = urlpart.indexOf('?'); + + if (questionMarkIndex == -1) { + return urlpart; + } + + if (!containsParamToRedact(urlpart)) { + return urlpart; + } + + StringBuilder redactedParameters = new StringBuilder(); + boolean paramToRedact = false; + boolean paramNameDetected = false; + boolean reference = false; + + StringBuilder currentParamName = new StringBuilder(); + + for (int i = questionMarkIndex + 1; i < urlpart.length(); i++) { + char currentChar = urlpart.charAt(i); + if (currentChar == '=') { + paramNameDetected = true; + redactedParameters.append(currentParamName); + redactedParameters.append('='); + if (PARAMS_TO_REDACT.contains(currentParamName.toString())) { + redactedParameters.append("REDACTED"); + paramToRedact = true; + } + } else if (currentChar == '&') { + redactedParameters.append('&'); + paramNameDetected = false; + paramToRedact = false; + currentParamName.setLength(0); + } else if (currentChar == '#') { + reference = true; + redactedParameters.append('#'); + } else if (!paramNameDetected) { + currentParamName.append(currentChar); + } else if (!paramToRedact || reference) { + redactedParameters.append(currentChar); + } + } + return urlpart.substring(0, questionMarkIndex) + "?" + redactedParameters; + } + + private static boolean containsParamToRedact(String urlpart) { + for (String param : PARAMS_TO_REDACT) { + if (urlpart.contains(param)) { + return true; + } } - return url.substring(0, schemeEndIndex + 3) + "REDACTED:REDACTED" + url.substring(atIndex); + return false; } } diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java index f4760f0201d3..701f040aa383 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java @@ -23,6 +23,7 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.params.provider.Arguments.arguments; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; @@ -36,9 +37,13 @@ import java.util.List; import java.util.Map; import java.util.function.ToIntFunction; +import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.ValueSource; @@ -200,6 +205,78 @@ void normal() { entry(NETWORK_PEER_PORT, 456L)); } + @ParameterizedTest + @ArgumentsSource(StripUrlArgumentSource.class) + void stripBasicAuthTest(String url, String expectedResult) { + Map<String, String> request = new HashMap<>(); + request.put("urlFull", url); + + System.setProperty( + "otel.instrumentation.http.client.experimental.redact-sensitive-parameters", "true"); + AttributesExtractor<Map<String, String>, Map<String, String>> extractor = + HttpClientAttributesExtractor.create(new TestHttpClientAttributesGetter()); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + + assertThat(attributes.build()).containsOnly(entry(URL_FULL, expectedResult)); + } + + static final class StripUrlArgumentSource implements ArgumentsProvider { + + @Override + public Stream<? extends Arguments> provideArguments(ExtensionContext context) { + return Stream.of( + arguments("https://user1:secret@github.com", "https://REDACTED:REDACTED@github.com"), + arguments( + "https://user1:secret@github.com/path/", + "https://REDACTED:REDACTED@github.com/path/"), + arguments( + "https://user1:secret@github.com#test.html", + "https://REDACTED:REDACTED@github.com#test.html"), + arguments( + "https://user1:secret@github.com?foo=b@r", + "https://REDACTED:REDACTED@github.com?foo=b@r"), + arguments( + "https://user1:secret@github.com/p@th?foo=b@r", + "https://REDACTED:REDACTED@github.com/p@th?foo=b@r"), + arguments("https://github.com/p@th?foo=b@r", "https://github.com/p@th?foo=b@r"), + arguments("https://github.com#t@st.html", "https://github.com#t@st.html"), + arguments("user1:secret@github.com", "user1:secret@github.com"), + arguments("https://github.com@", "https://github.com@"), + arguments( + "https://service.com?paramA=valA¶mB=valB", + "https://service.com?paramA=valA¶mB=valB"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7", + "https://service.com?AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0%3A377", + "https://service.com?Signature=REDACTED"), + arguments( + "https://service.com?sig=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0", + "https://service.com?sig=REDACTED"), + arguments( + "https://service.com?X-Goog-Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0", + "https://service.com?X-Goog-Signature=REDACTED"), + arguments( + "https://service.com?paramA=valA&AWSAccessKeyId=AKIAIOSFODNN7¶mB=valB", + "https://service.com?paramA=valA&AWSAccessKeyId=REDACTED¶mB=valB"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7¶mA=valA", + "https://service.com?AWSAccessKeyId=REDACTED¶mA=valA"), + arguments( + "https://service.com?paramA=valA&AWSAccessKeyId=AKIAIOSFODNN7", + "https://service.com?paramA=valA&AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7&AWSAccessKeyId=ZGIAIOSFODNN7", + "https://service.com?AWSAccessKeyId=REDACTED&AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7#ref", + "https://service.com?AWSAccessKeyId=REDACTED#ref")); + } + } + @ParameterizedTest @ArgumentsSource(ValidRequestMethodsProvider.class) void shouldExtractKnownMethods(String requestMethod) {