Skip to content

Commit b4614e4

Browse files
authored
Hide the value of sensitive query parameters in log (#8242)
Add option to redact sensitive query params.
1 parent c299d62 commit b4614e4

File tree

3 files changed

+125
-2
lines changed

3 files changed

+125
-2
lines changed

okhttp-logging-interceptor/api/logging-interceptor.api

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public final class okhttp3/logging/HttpLoggingInterceptor : okhttp3/Interceptor
88
public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response;
99
public final fun level (Lokhttp3/logging/HttpLoggingInterceptor$Level;)V
1010
public final fun redactHeader (Ljava/lang/String;)V
11+
public final fun redactQueryParams ([Ljava/lang/String;)V
1112
public final fun setLevel (Lokhttp3/logging/HttpLoggingInterceptor$Level;)Lokhttp3/logging/HttpLoggingInterceptor;
1213
}
1314

okhttp-logging-interceptor/src/main/kotlin/okhttp3/logging/HttpLoggingInterceptor.kt

+26-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import java.nio.charset.Charset
2222
import java.util.TreeSet
2323
import java.util.concurrent.TimeUnit
2424
import okhttp3.Headers
25+
import okhttp3.HttpUrl
2526
import okhttp3.Interceptor
2627
import okhttp3.OkHttpClient
2728
import okhttp3.Response
@@ -46,6 +47,8 @@ class HttpLoggingInterceptor
4647
) : Interceptor {
4748
@Volatile private var headersToRedact = emptySet<String>()
4849

50+
@Volatile private var queryParamsNameToRedact = emptySet<String>()
51+
4952
@set:JvmName("level")
5053
@Volatile
5154
var level = Level.NONE
@@ -132,6 +135,13 @@ class HttpLoggingInterceptor
132135
headersToRedact = newHeadersToRedact
133136
}
134137

138+
fun redactQueryParams(vararg name: String) {
139+
val newQueryParamsNameToRedact = TreeSet(String.CASE_INSENSITIVE_ORDER)
140+
newQueryParamsNameToRedact += queryParamsNameToRedact
141+
newQueryParamsNameToRedact.addAll(name)
142+
queryParamsNameToRedact = newQueryParamsNameToRedact
143+
}
144+
135145
/**
136146
* Sets the level and returns this.
137147
*
@@ -168,7 +178,7 @@ class HttpLoggingInterceptor
168178

169179
val connection = chain.connection()
170180
var requestStartMessage =
171-
("--> ${request.method} ${request.url}${if (connection != null) " " + connection.protocol() else ""}")
181+
("--> ${request.method} ${redactUrl(request.url)}${if (connection != null) " " + connection.protocol() else ""}")
172182
if (!logHeaders && requestBody != null) {
173183
requestStartMessage += " (${requestBody.contentLength()}-byte body)"
174184
}
@@ -251,7 +261,7 @@ class HttpLoggingInterceptor
251261
buildString {
252262
append("<-- ${response.code}")
253263
if (response.message.isNotEmpty()) append(" ${response.message}")
254-
append(" ${response.request.url} (${tookMs}ms")
264+
append(" ${redactUrl(response.request.url)} (${tookMs}ms")
255265
if (!logHeaders) append(", $bodySize body")
256266
append(")")
257267
},
@@ -312,6 +322,20 @@ class HttpLoggingInterceptor
312322
return response
313323
}
314324

325+
internal fun redactUrl(url: HttpUrl): String {
326+
if (queryParamsNameToRedact.isEmpty() || url.querySize == 0) {
327+
return url.toString()
328+
}
329+
return url.newBuilder().query(null).apply {
330+
for (i in 0 until url.querySize) {
331+
val parameterName = url.queryParameterName(i)
332+
val newValue = if (parameterName in queryParamsNameToRedact) "██" else url.queryParameterValue(i)
333+
334+
addEncodedQueryParameter(parameterName, newValue)
335+
}
336+
}.toString()
337+
}
338+
315339
private fun logHeader(
316340
headers: Headers,
317341
i: Int,

okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.kt

+98
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,104 @@ class HttpLoggingInterceptorTest {
903903
.assertNoMoreLogs()
904904
}
905905

906+
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
907+
@Test
908+
fun sensitiveQueryParamsAreRedacted() {
909+
url = server.url("/api/login?user=test_user&authentication=basic&password=confidential_password")
910+
val networkInterceptor =
911+
HttpLoggingInterceptor(networkLogs).setLevel(
912+
Level.BASIC,
913+
)
914+
networkInterceptor.redactQueryParams("user", "passWord")
915+
916+
val applicationInterceptor =
917+
HttpLoggingInterceptor(applicationLogs).setLevel(
918+
Level.BASIC,
919+
)
920+
applicationInterceptor.redactQueryParams("user", "PassworD")
921+
922+
client =
923+
OkHttpClient.Builder()
924+
.addNetworkInterceptor(networkInterceptor)
925+
.addInterceptor(applicationInterceptor)
926+
.build()
927+
server.enqueue(
928+
MockResponse.Builder()
929+
.build(),
930+
)
931+
val response =
932+
client
933+
.newCall(
934+
request()
935+
.build(),
936+
)
937+
.execute()
938+
response.body.close()
939+
val redactedUrl = networkInterceptor.redactUrl(url)
940+
val redactedUrlPattern = redactedUrl.replace("?", """\?""")
941+
applicationLogs
942+
.assertLogEqual("--> GET $redactedUrl")
943+
.assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)"""))
944+
.assertNoMoreLogs()
945+
networkLogs
946+
.assertLogEqual("--> GET $redactedUrl http/1.1")
947+
.assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)"""))
948+
.assertNoMoreLogs()
949+
}
950+
951+
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
952+
@Test
953+
fun preserveQueryParamsAfterRedacted() {
954+
url =
955+
server.url(
956+
"""/api/login?
957+
|user=test_user&
958+
|authentication=basic&
959+
|password=confidential_password&
960+
|authentication=rather simple login method
961+
""".trimMargin(),
962+
)
963+
val networkInterceptor =
964+
HttpLoggingInterceptor(networkLogs).setLevel(
965+
Level.BASIC,
966+
)
967+
networkInterceptor.redactQueryParams("user", "passWord")
968+
969+
val applicationInterceptor =
970+
HttpLoggingInterceptor(applicationLogs).setLevel(
971+
Level.BASIC,
972+
)
973+
applicationInterceptor.redactQueryParams("user", "PassworD")
974+
975+
client =
976+
OkHttpClient.Builder()
977+
.addNetworkInterceptor(networkInterceptor)
978+
.addInterceptor(applicationInterceptor)
979+
.build()
980+
server.enqueue(
981+
MockResponse.Builder()
982+
.build(),
983+
)
984+
val response =
985+
client
986+
.newCall(
987+
request()
988+
.build(),
989+
)
990+
.execute()
991+
response.body.close()
992+
val redactedUrl = networkInterceptor.redactUrl(url)
993+
val redactedUrlPattern = redactedUrl.replace("?", """\?""")
994+
applicationLogs
995+
.assertLogEqual("--> GET $redactedUrl")
996+
.assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)"""))
997+
.assertNoMoreLogs()
998+
networkLogs
999+
.assertLogEqual("--> GET $redactedUrl http/1.1")
1000+
.assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)"""))
1001+
.assertNoMoreLogs()
1002+
}
1003+
9061004
@Test
9071005
fun duplexRequestsAreNotLogged() {
9081006
platform.assumeHttp2Support()

0 commit comments

Comments
 (0)