Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL-encode untrusted data in URLs #5798

Merged
merged 7 commits into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand All @@ -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
),
Expand All @@ -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
)
Expand Down
3 changes: 3 additions & 0 deletions stripe-core/api/stripe-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,6 @@ class SharedPreferencesStorage(
}

private companion object {
val logTag: String = SharedPreferencesStorage::class.java.simpleName
private val logTag: String = SharedPreferencesStorage::class.java.simpleName
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -14,29 +16,40 @@ private val json = Json {
encodeDefaults = true
}

internal fun b64Encode(s: String): String =
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
fun b64Encode(s: String): String =
awush-stripe marked this conversation as resolved.
Show resolved Hide resolved
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 <T> encodeToXWWWFormUrl(serializer: SerializationStrategy<T>, value: T): String =
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
fun <T> encodeToXWWWFormUrl(serializer: SerializationStrategy<T>, value: T): String =
QueryStringFactory.createFromParamsWithEmptyValues(
json.encodeToJsonElement(serializer, value).toMap()
)

/**
* Encode a serializable object to a JSON string
*/
internal fun <T> encodeToJson(serializer: SerializationStrategy<T>, value: T): String =
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
fun <T> encodeToJson(serializer: SerializationStrategy<T>, value: T): String =
json.encodeToString(serializer, value)

/**
* Decode an object from a JSON string
*/
internal fun <T> decodeFromJson(deserializer: DeserializationStrategy<T>, value: String): T =
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
fun <T> decodeFromJson(deserializer: DeserializationStrategy<T>, 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())
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down