mirror of
https://github.com/bitwarden/android.git
synced 2026-05-07 11:29:37 -05:00
[PM-24380] fix: Correct and redact flight recorder hostname on logs (#6633)
This commit is contained in:
@@ -2,6 +2,7 @@ package com.bitwarden.data.manager.flightrecorder
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.manager.BuildInfoManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
@@ -10,6 +11,7 @@ import com.bitwarden.core.data.util.toFormattedPattern
|
||||
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
|
||||
import com.bitwarden.data.manager.file.FileManager
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.network.util.redactHostnamesInMessage
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedWriter
|
||||
@@ -34,6 +36,21 @@ internal class FlightRecorderWriterImpl(
|
||||
private val buildInfoManager: BuildInfoManager,
|
||||
private val serverConfigRepository: ServerConfigRepository,
|
||||
) : FlightRecorderWriter {
|
||||
private val configuredHosts: Set<String>
|
||||
get() {
|
||||
val environment = serverConfigRepository.serverConfigStateFlow.value
|
||||
?.serverData?.environment ?: return emptySet()
|
||||
return listOfNotNull(
|
||||
environment.vaultUrl,
|
||||
environment.apiUrl,
|
||||
environment.identityUrl,
|
||||
environment.notificationsUrl,
|
||||
environment.ssoUrl,
|
||||
)
|
||||
.mapNotNull { it.toUri().host }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData) {
|
||||
fileManager.delete(File(File(fileManager.logsDirectory), data.fileName))
|
||||
}
|
||||
@@ -98,6 +115,7 @@ internal class FlightRecorderWriterImpl(
|
||||
val formattedTime = clock
|
||||
.instant()
|
||||
.toFormattedPattern(pattern = LOG_TIME_PATTERN, clock = clock)
|
||||
val hosts = configuredHosts
|
||||
withContext(context = dispatcherManager.io) {
|
||||
runCatching {
|
||||
BufferedWriter(FileWriter(logFile, true)).use { bw ->
|
||||
@@ -109,10 +127,10 @@ internal class FlightRecorderWriterImpl(
|
||||
bw.append(it)
|
||||
}
|
||||
bw.append(" – ")
|
||||
bw.append(message)
|
||||
bw.append(message.redactHostnamesInMessage(hosts))
|
||||
throwable?.let {
|
||||
bw.append(" – ")
|
||||
bw.append(it.getStackTraceString())
|
||||
bw.append(it.getStackTraceString().redactHostnamesInMessage(hosts))
|
||||
}
|
||||
bw.newLine()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import java.lang.reflect.Type
|
||||
*/
|
||||
private const val NO_CONTENT_RESPONSE_CODE: Int = 204
|
||||
|
||||
private val UNKNOWN_HOST_REGEX = Regex("""Unable to resolve host "([^"]+)"""")
|
||||
|
||||
/**
|
||||
* A [Call] for wrapping a network request into a [NetworkResult].
|
||||
*/
|
||||
@@ -67,8 +69,16 @@ internal class NetworkResultCall<T>(
|
||||
fun executeForResult(): NetworkResult<T> = requireNotNull(execute().body())
|
||||
|
||||
private fun Throwable.toFailure(): NetworkResult<T> {
|
||||
// We rebuild the URL without query params, we do not want to log those
|
||||
val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
|
||||
val originalUrl = backingCall.request().url.toUrl()
|
||||
|
||||
val extractedHost = message?.let { UNKNOWN_HOST_REGEX.find(it)?.groupValues?.getOrNull(1) }
|
||||
|
||||
val url = if (extractedHost != null) {
|
||||
"${originalUrl.protocol}://$extractedHost${originalUrl.path}"
|
||||
} else {
|
||||
"${originalUrl.protocol}://${originalUrl.authority}${originalUrl.path}"
|
||||
}
|
||||
|
||||
Timber.w(this, "Network Error: $url")
|
||||
return NetworkResult.Failure(this)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.bitwarden.network.util
|
||||
|
||||
/**
|
||||
* List of official Bitwarden cloud hostnames that are safe to log.
|
||||
*/
|
||||
private val BITWARDEN_HOSTS = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")
|
||||
|
||||
/**
|
||||
* Redacts hostnames in a log message by replacing bare hostnames with [REDACTED_SELF_HOST].
|
||||
*
|
||||
* Only redacts hostnames that match [configuredHosts] AND are not official Bitwarden domains.
|
||||
* Preserves all Bitwarden domains (including QA/dev environments).
|
||||
*
|
||||
* @param configuredHosts Set of hostnames to redact
|
||||
* @return Message with hostnames redacted as [REDACTED_SELF_HOST]
|
||||
*/
|
||||
fun String.redactHostnamesInMessage(configuredHosts: Set<String>): String =
|
||||
configuredHosts.fold(this) { result, hostname ->
|
||||
val escapedHostname = Regex.escape(hostname)
|
||||
val bareHostnamePattern = Regex("""\b$escapedHostname\b""")
|
||||
bareHostnamePattern.replace(result) { hostname.redactIfSelfHosted() }
|
||||
}
|
||||
|
||||
private fun String.redactIfSelfHosted(): String {
|
||||
val isBitwardenHost = BITWARDEN_HOSTS.any { this.endsWith(it) }
|
||||
return if (isBitwardenHost) this else "[REDACTED_SELF_HOST]"
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.bitwarden.network.util
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class HostnameRedactionUtilTest {
|
||||
@Test
|
||||
fun `redactHostnamesInMessage redacts configured self-hosted URLs`() {
|
||||
val message = "--> GET https://vault.example.com/api/sync HTTP/1.1"
|
||||
val configuredHosts = setOf("vault.example.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals("--> GET https://[REDACTED_SELF_HOST]/api/sync HTTP/1.1", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage preserves non-configured URLs`() {
|
||||
val message = "--> GET https://vault.example.com/api/sync HTTP/1.1"
|
||||
val configuredHosts = setOf("api.bitwarden.com") // Different host
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(message, result) // Unchanged - not in configured hosts
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage preserves Bitwarden URLs even if configured`() {
|
||||
val message = "--> GET https://vault.bitwarden.com/api/sync HTTP/1.1"
|
||||
val configuredHosts = setOf("vault.bitwarden.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(message, result) // Unchanged - Bitwarden domain preserved
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage redacts quoted hostnames in error messages`() {
|
||||
val message = """Unable to resolve host "vault.example.com": No address"""
|
||||
val configuredHosts = setOf("vault.example.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals("""Unable to resolve host "[REDACTED_SELF_HOST]": No address""", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage handles multiple URLs in one message`() {
|
||||
val message = "Redirect from https://old.corp.com to https://new.corp.com"
|
||||
val configuredHosts = setOf("old.corp.com", "new.corp.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(
|
||||
"Redirect from https://[REDACTED_SELF_HOST] to https://[REDACTED_SELF_HOST]",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage handles empty configured hosts`() {
|
||||
val message = "--> GET https://vault.example.com/api HTTP/1.1"
|
||||
val configuredHosts = emptySet<String>()
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(message, result) // Unchanged - no hosts to redact
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage handles NetworkCookieManagerImpl getCookies pattern`() {
|
||||
val message = "2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"getCookies(vault.example.com): resolved=vault.example.com, count=0"
|
||||
val configuredHosts = setOf("vault.example.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(
|
||||
"2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"getCookies([REDACTED_SELF_HOST]): resolved=[REDACTED_SELF_HOST], count=0",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage preserves Bitwarden domains in NetworkCookieManagerImpl logs`() {
|
||||
val message = "2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"getCookies(vault.example.com): resolved=vault.qa.bitwarden.pw, count=0"
|
||||
val configuredHosts = setOf("vault.example.com", "vault.qa.bitwarden.pw")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(
|
||||
"2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"getCookies([REDACTED_SELF_HOST]): resolved=vault.qa.bitwarden.pw, count=0",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage handles UnknownHostException error message`() {
|
||||
val message = "DEBUG – BitwardenNetworkClient – <-- HTTP FAILED: " +
|
||||
"java.net.UnknownHostException: Unable to resolve host " +
|
||||
"\"vault.example.com\": No address associated with hostname."
|
||||
val configuredHosts = setOf("vault.example.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(
|
||||
"DEBUG – BitwardenNetworkClient – <-- HTTP FAILED: " +
|
||||
"java.net.UnknownHostException: Unable to resolve host " +
|
||||
"\"[REDACTED_SELF_HOST]\": No address associated with hostname.",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage handles needsBootstrap pattern`() {
|
||||
val message = "2026-03-09 12:43:29:851 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"needsBootstrap(vault.example.com): false (cookieDomain=null)"
|
||||
val configuredHosts = setOf("vault.example.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(
|
||||
"2026-03-09 12:43:29:851 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"needsBootstrap([REDACTED_SELF_HOST]): false (cookieDomain=null)",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redactHostnamesInMessage handles resolveHostname pattern`() {
|
||||
val message = "2026-03-09 12:43:29:855 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"resolveHostname(vault.example.com): no stored config found, using original"
|
||||
val configuredHosts = setOf("vault.example.com")
|
||||
|
||||
val result = message.redactHostnamesInMessage(configuredHosts)
|
||||
|
||||
assertEquals(
|
||||
"2026-03-09 12:43:29:855 – DEBUG – NetworkCookieManagerImpl – " +
|
||||
"resolveHostname([REDACTED_SELF_HOST]): no stored config found, using original",
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user