AzureAD / microsoft-authentication-library-common-for-android

Common code used by both the Active Directory Authentication Library (ADAL) and the Microsoft Authentication Library (MSAL)
MIT License
41 stars 35 forks source link

Safe logging #2384

Closed SammyO closed 6 months ago

SammyO commented 6 months ago

The default implementation of toString() of Kotlin's data classes uses all fields to compose the String. This unintentionally includes fields that may not be safe for logging.

Summary of solution:

Note 1: it would have been more efficient to have Logging define override fun toString(), meaning every class would be required to override toString(). However, this is not possible in Kotlin (see link).

Note 2: the issue that this PR addresses applies to data classes. However, this PR also includes some updates to regular classes, and thereby partially addressing https://identitydivision.visualstudio.com/Engineering/_workitems/edit/2811937

Accompanying MSAL PR: https://github.com/AzureAD/microsoft-authentication-library-for-android/pull/2081

SammyO commented 6 months ago

My preferred approach would have been the design suggested by Giannis in Teams chat where the toString method is implemented only once in the abstract Logging class. As multiple inheritance is not supported, we had to rule out that option. So, we are left with a situation where we have to override toString method in all classes. In this scenario I would prefer the toString method to be the "safe" option by default and create another method toUnsafeString() that would return a string containing PII values. This would mean the caller of the classes by default get the benefit of "safety". If anyone wants a string with PII then they must call the toUnsafeString() method.

The interface would look like

interface Logging {
    fun toUnsafeString(): String

    fun containsPii(): Boolean
}

data class UnknownError(
    override val error: String?,
    override val errorDescription: String?,
    override val details: List<Map<String, String>>? = null,
    override val correlationId: String,
    override val errorCodes: List<Int>? = null,
    val exception: Exception? = null
): Error(error, errorDescription, details, correlationId, errorCodes), INativeAuthCommandResult,
    SignInStartCommandResult, SignInWithContinuationTokenCommandResult, SignInSubmitCodeCommandResult,
    SignInResendCodeCommandResult, SignInSubmitPasswordCommandResult,
    SignUpStartCommandResult, SignUpSubmitUserAttributesCommandResult,
    SignUpSubmitCodeCommandResult, SignUpResendCodeCommandResult,
    SignUpSubmitPasswordCommandResult,
    ResetPasswordStartCommandResult, ResetPasswordSubmitCodeCommandResult,
    ResetPasswordResendCodeCommandResult, ResetPasswordSubmitNewPasswordCommandResult {    

    override fun toUnsafeString(): String { return "UnknownError(correlationId=$correlationId, error=$error, errorDescription=$errorDescription), details=$details, errorCodes=$errorCodes" }

    override fun containsPii(): Boolean = true

    override fun toString(): String { return "UnknownError(correlationId=$correlationId)" }
}

This design has the same complexity but is more readable as the developer does not have to understand the meaning of the parameter

@SaurabhMSFT I see your point. I don't like this exact approach though, because:

can you think of a way to address the above? For example, by renaming mayContainPii and setting a default value?

SaurabhMSFT commented 6 months ago

My preferred approach would have been the design suggested by Giannis in Teams chat where the toString method is implemented only once in the abstract Logging class. As multiple inheritance is not supported, we had to rule out that option. So, we are left with a situation where we have to override toString method in all classes. In this scenario I would prefer the toString method to be the "safe" option by default and create another method toUnsafeString() that would return a string containing PII values. This would mean the caller of the classes by default get the benefit of "safety". If anyone wants a string with PII then they must call the toUnsafeString() method. The interface would look like

interface Logging {
    fun toUnsafeString(): String

    fun containsPii(): Boolean
}

data class UnknownError(
    override val error: String?,
    override val errorDescription: String?,
    override val details: List<Map<String, String>>? = null,
    override val correlationId: String,
    override val errorCodes: List<Int>? = null,
    val exception: Exception? = null
): Error(error, errorDescription, details, correlationId, errorCodes), INativeAuthCommandResult,
    SignInStartCommandResult, SignInWithContinuationTokenCommandResult, SignInSubmitCodeCommandResult,
    SignInResendCodeCommandResult, SignInSubmitPasswordCommandResult,
    SignUpStartCommandResult, SignUpSubmitUserAttributesCommandResult,
    SignUpSubmitCodeCommandResult, SignUpResendCodeCommandResult,
    SignUpSubmitPasswordCommandResult,
    ResetPasswordStartCommandResult, ResetPasswordSubmitCodeCommandResult,
    ResetPasswordResendCodeCommandResult, ResetPasswordSubmitNewPasswordCommandResult {    

    override fun toUnsafeString(): String { return "UnknownError(correlationId=$correlationId, error=$error, errorDescription=$errorDescription), details=$details, errorCodes=$errorCodes" }

    override fun containsPii(): Boolean = true

    override fun toString(): String { return "UnknownError(correlationId=$correlationId)" }
}

This design has the same complexity but is more readable as the developer does not have to understand the meaning of the parameter

@SaurabhMSFT I see your point. I don't like this exact approach though, because:

  • toUnsafeString() doesn't apply to all classes; not all classes have unsafe parameters. It depends on the value of containsPii(). The term unsafe string gives a false impression that the string is always unsafe, which it isn't.
  • the current toSafeString() returns a string that given the context is considered safe (either by removing PII, or by including it if the developer wants to add it, in which case it's also safe).

can you think of a way to address the above? For example, by renaming mayContainPii and setting a default value?

We can call the method `toUnsanitizedString()'. That would should the intent that the output of this method may contain PII.