diff --git a/CHANGELOG.md b/CHANGELOG.md index 33765cc2975..d4649612e4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## XX.XX.XX - 2022-XX-XX +### CardScan +* [SECURITY][5798](https://github.com/stripe/stripe-android/pull/5798) URL-encode IDs used in URLs to prevent injection attacks. + ## 20.16.0 - 2022-11-14 ### Payments diff --git a/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt b/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt index 2220e26d89f..8b6eba9b72d 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt @@ -17,6 +17,7 @@ import com.stripe.android.core.networking.ApiRequest import com.stripe.android.core.networking.StripeNetworkClient import com.stripe.android.core.networking.StripeRequest import com.stripe.android.core.networking.responseJson +import com.stripe.android.core.utils.urlEncode import com.stripe.android.identity.networking.models.ClearDataParam import com.stripe.android.identity.networking.models.ClearDataParam.Companion.createCollectedDataParamEntry import com.stripe.android.identity.networking.models.CollectedDataParam @@ -53,7 +54,7 @@ internal class DefaultIdentityRepository @Inject constructor( ephemeralKey: String ): VerificationPage = executeRequestWithKSerializer( apiRequestFactory.createGet( - url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/$id", + url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/${urlEncode(id)}", options = ApiRequest.Options( apiKey = ephemeralKey ) @@ -68,7 +69,7 @@ internal class DefaultIdentityRepository @Inject constructor( clearDataParam: ClearDataParam ): VerificationPageData = executeRequestWithKSerializer( apiRequestFactory.createPost( - url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/$id/$DATA", + url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/${urlEncode(id)}/$DATA", options = ApiRequest.Options( apiKey = ephemeralKey ), @@ -85,7 +86,7 @@ internal class DefaultIdentityRepository @Inject constructor( ephemeralKey: String ): VerificationPageData = executeRequestWithKSerializer( apiRequestFactory.createPost( - url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/$id/$SUBMIT", + url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/${urlEncode(id)}/$SUBMIT", options = ApiRequest.Options( apiKey = ephemeralKey ) diff --git a/stripe-core/api/stripe-core.api b/stripe-core/api/stripe-core.api index 10b59776655..d7752b8464f 100644 --- a/stripe-core/api/stripe-core.api +++ b/stripe-core/api/stripe-core.api @@ -347,6 +347,9 @@ public final class com/stripe/android/core/networking/StripeRequest$MimeType : j public static fun values ()[Lcom/stripe/android/core/networking/StripeRequest$MimeType; } +public final class com/stripe/android/core/utils/EncodeKt { +} + public final class com/stripe/android/core/version/StripeSdkVersion { public static final field INSTANCE Lcom/stripe/android/core/version/StripeSdkVersion; public static final field VERSION Ljava/lang/String; diff --git a/stripe-core/src/main/java/com/stripe/android/core/storage/Storage.kt b/stripe-core/src/main/java/com/stripe/android/core/storage/Storage.kt index 97a7714ddda..4ff2dfa4d6e 100644 --- a/stripe-core/src/main/java/com/stripe/android/core/storage/Storage.kt +++ b/stripe-core/src/main/java/com/stripe/android/core/storage/Storage.kt @@ -223,6 +223,6 @@ class SharedPreferencesStorage( } private companion object { - val logTag: String = SharedPreferencesStorage::class.java.simpleName + private val logTag: String = SharedPreferencesStorage::class.java.simpleName } } diff --git a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/util/Encode.kt b/stripe-core/src/main/java/com/stripe/android/core/utils/Encode.kt similarity index 52% rename from stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/util/Encode.kt rename to stripe-core/src/main/java/com/stripe/android/core/utils/Encode.kt index 91d6e4db866..fea44dc6e16 100644 --- a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/util/Encode.kt +++ b/stripe-core/src/main/java/com/stripe/android/core/utils/Encode.kt @@ -1,11 +1,13 @@ -package com.stripe.android.stripecardscan.framework.util +package com.stripe.android.core.utils import android.util.Base64 +import androidx.annotation.RestrictTo import com.stripe.android.core.networking.QueryStringFactory import com.stripe.android.core.networking.toMap import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.json.Json +import java.net.URLEncoder import java.nio.charset.Charset private val json = Json { @@ -14,17 +16,20 @@ private val json = Json { encodeDefaults = true } -internal fun b64Encode(s: String): String = +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +fun b64Encode(s: String): String = Base64.encodeToString(s.toByteArray(Charset.defaultCharset()), Base64.NO_WRAP) -internal fun b64Encode(b: ByteArray): String = +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +fun b64Encode(b: ByteArray): String = Base64.encodeToString(b, Base64.NO_WRAP) /** * Encode a serializable object to a x-www-url-encoded string. The source object must convert to a * [Map] so that the parameters can be named. */ -internal fun encodeToXWWWFormUrl(serializer: SerializationStrategy, value: T): String = +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +fun encodeToXWWWFormUrl(serializer: SerializationStrategy, value: T): String = QueryStringFactory.createFromParamsWithEmptyValues( json.encodeToJsonElement(serializer, value).toMap() ) @@ -32,11 +37,19 @@ internal fun encodeToXWWWFormUrl(serializer: SerializationStrategy, value /** * Encode a serializable object to a JSON string */ -internal fun encodeToJson(serializer: SerializationStrategy, value: T): String = +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +fun encodeToJson(serializer: SerializationStrategy, value: T): String = json.encodeToString(serializer, value) /** * Decode an object from a JSON string */ -internal fun decodeFromJson(deserializer: DeserializationStrategy, value: String): T = +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +fun decodeFromJson(deserializer: DeserializationStrategy, value: String): T = json.decodeFromString(deserializer, value) + +/** + * URL-encode a string. This is useful for sanitizing untrusted data for use in URLs. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +fun urlEncode(value: String): String = URLEncoder.encode(value, Charsets.UTF_8.name()) diff --git a/stripecardscan/src/androidTest/java/com/stripe/android/stripecardscan/framework/api/dto/CardImageVerificationDetailsTest.kt b/stripecardscan/src/androidTest/java/com/stripe/android/stripecardscan/framework/api/dto/CardImageVerificationDetailsTest.kt index 5ee69436719..9a7646b6560 100644 --- a/stripecardscan/src/androidTest/java/com/stripe/android/stripecardscan/framework/api/dto/CardImageVerificationDetailsTest.kt +++ b/stripecardscan/src/androidTest/java/com/stripe/android/stripecardscan/framework/api/dto/CardImageVerificationDetailsTest.kt @@ -4,7 +4,7 @@ import android.util.Size import androidx.test.filters.SmallTest import com.stripe.android.stripecardscan.framework.util.AcceptedImageConfigs import com.stripe.android.stripecardscan.framework.util.ImageFormat -import com.stripe.android.stripecardscan.framework.util.decodeFromJson +import com.stripe.android.core.utils.decodeFromJson import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/Network.kt b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/Network.kt index 76f4da05faf..f6f43baf691 100644 --- a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/Network.kt +++ b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/Network.kt @@ -3,9 +3,9 @@ package com.stripe.android.stripecardscan.framework.api import android.util.Log import com.stripe.android.camera.framework.time.Duration import com.stripe.android.camera.framework.time.seconds +import com.stripe.android.core.utils.decodeFromJson +import com.stripe.android.core.utils.encodeToXWWWFormUrl import com.stripe.android.stripecardscan.framework.api.StripeNetwork.Companion.RESPONSE_CODE_UNSET -import com.stripe.android.stripecardscan.framework.util.decodeFromJson -import com.stripe.android.stripecardscan.framework.util.encodeToXWWWFormUrl import com.stripe.android.stripecardscan.framework.util.retry import kotlinx.serialization.KSerializer import java.io.File diff --git a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeApi.kt b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeApi.kt index 0a807ed0568..1384b19f4bb 100644 --- a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeApi.kt +++ b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeApi.kt @@ -5,6 +5,8 @@ package com.stripe.android.stripecardscan.framework.api import android.util.Log import androidx.annotation.CheckResult +import com.stripe.android.core.utils.encodeToJson +import com.stripe.android.core.utils.urlEncode import com.stripe.android.stripecardscan.cardimageverification.SavedFrame import com.stripe.android.stripecardscan.framework.api.dto.AppInfo import com.stripe.android.stripecardscan.framework.api.dto.CardImageVerificationDetailsRequest @@ -24,7 +26,6 @@ import com.stripe.android.stripecardscan.framework.api.dto.VerifyFramesResult import com.stripe.android.stripecardscan.framework.util.AppDetails import com.stripe.android.stripecardscan.framework.util.Device import com.stripe.android.stripecardscan.framework.util.ScanConfig -import com.stripe.android.stripecardscan.framework.util.encodeToJson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -72,7 +73,7 @@ internal fun uploadScanStatsCIV( when ( val result = network.postForResult( stripePublishableKey = stripePublishableKey, - path = "/card_image_verifications/$civId/scan_stats", + path = "/card_image_verifications/${urlEncode(civId)}/scan_stats", data = ScanStatsCIVRequest( clientSecret = civSecret, payload = statsPayload @@ -152,7 +153,7 @@ internal suspend fun getCardImageVerificationIntentDetails( ) = withContext(Dispatchers.IO) { network.postForResult( stripePublishableKey = stripePublishableKey, - path = "/card_image_verifications/$civId/initialize_client", + path = "/card_image_verifications/${urlEncode(civId)}/initialize_client", data = CardImageVerificationDetailsRequest(civSecret), requestSerializer = CardImageVerificationDetailsRequest.serializer(), responseSerializer = CardImageVerificationDetailsResult.serializer(), @@ -170,7 +171,7 @@ internal suspend fun uploadSavedFrames( ) = withContext(Dispatchers.IO) { network.postForResult( stripePublishableKey = stripePublishableKey, - path = "card_image_verifications/$civId/verify_frames", + path = "card_image_verifications/${urlEncode(civId)}/verify_frames", data = VerifyFramesRequest( clientSecret = civSecret, verificationFramesData = encodeToJson( diff --git a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeNetwork.kt b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeNetwork.kt index 59f6eaa2e99..d76b4dfef3f 100644 --- a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeNetwork.kt +++ b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/api/StripeNetwork.kt @@ -4,10 +4,10 @@ import android.util.Log import com.stripe.android.core.networking.DefaultStripeNetworkClient import com.stripe.android.core.networking.StripeNetworkClient import com.stripe.android.core.networking.StripeRequest +import com.stripe.android.core.utils.decodeFromJson +import com.stripe.android.core.utils.encodeToXWWWFormUrl import com.stripe.android.stripecardscan.framework.api.dto.CardScanFileDownloadRequest import com.stripe.android.stripecardscan.framework.api.dto.CardScanRequest -import com.stripe.android.stripecardscan.framework.util.decodeFromJson -import com.stripe.android.stripecardscan.framework.util.encodeToXWWWFormUrl import kotlinx.serialization.KSerializer import java.io.File import java.net.HttpURLConnection.HTTP_MULT_CHOICE diff --git a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/util/ToVerificationFrameData.kt b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/util/ToVerificationFrameData.kt index 22ed3a59080..5ffb868fa49 100644 --- a/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/util/ToVerificationFrameData.kt +++ b/stripecardscan/src/main/java/com/stripe/android/stripecardscan/framework/util/ToVerificationFrameData.kt @@ -4,6 +4,7 @@ import androidx.annotation.CheckResult import com.stripe.android.camera.framework.image.determineViewFinderCrop import com.stripe.android.camera.framework.image.size import com.stripe.android.camera.framework.util.move +import com.stripe.android.core.utils.b64Encode import com.stripe.android.stripecardscan.cardimageverification.SavedFrame import com.stripe.android.stripecardscan.framework.api.dto.PayloadInfo import com.stripe.android.stripecardscan.framework.api.dto.VerificationFrameData diff --git a/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/EncodeTest.kt b/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/EncodeTest.kt index 06f7c6d3aff..cbcd0463590 100644 --- a/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/EncodeTest.kt +++ b/stripecardscan/src/test/java/com/stripe/android/stripecardscan/framework/util/EncodeTest.kt @@ -1,6 +1,7 @@ package com.stripe.android.stripecardscan.framework.util import androidx.test.filters.SmallTest +import com.stripe.android.core.utils.encodeToXWWWFormUrl import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.junit.Test