Skip to content

Commit e938f42

Browse files
authored
Fix API parameters signature
1 parent 2a287ca commit e938f42

File tree

4 files changed

+61
-13
lines changed

4 files changed

+61
-13
lines changed

uploader/src/main/kotlin/com/cloudinary/upload/Uploader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ class Uploader internal constructor(val cloudinary: Cloudinary, clientFactory: H
189189
paramsMap["timestamp"] = paramsMap["timestamp"]
190190
?: (System.currentTimeMillis() / 1000L).asCloudinaryTimestamp()
191191
paramsMap["signature"] =
192-
paramsMap["signature"] ?: apiSignRequest(paramsMap, apiSecret)
192+
paramsMap["signature"] ?: apiSignRequest(paramsMap, apiSecret, config.urlConfig.signatureVersion)
193193
paramsMap["api_key"] = apiKey
194194
}
195195

uploader/src/main/kotlin/com/cloudinary/util/utils.kt

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,29 @@ fun randomPublicId(): String {
1717
return bytes.toHex()
1818
}
1919

20-
fun apiSignRequest(paramsToSign: MutableMap<String, Any>, apiSecret: String): String {
21-
val params = ArrayList<String>()
22-
paramsToSign.entries.forEach {
23-
val value = if (it.value is List<*>)
24-
(it.value as Collection<*>).joinToString(",")
25-
else
26-
it.value
27-
28-
params.add("${it.key}=${value}")
29-
}
20+
fun apiSignRequest(paramsToSign: Map<String, Any>, apiSecret: String, signatureVersion: Int): String {
21+
val queryString = paramsToSign.entries
22+
.map {
23+
val valueStr = if (it.value is List<*>) {
24+
(it.value as Collection<*>).joinToString(",")
25+
} else {
26+
it.value.toString()
27+
}
28+
29+
val sanitizedValue = if (signatureVersion == 2) {
30+
valueStr.replace("&", "%26")
31+
} else {
32+
valueStr
33+
}
34+
35+
"${it.key}=$sanitizedValue"
36+
}
37+
.filter { it.isNotBlank() }
38+
.sorted()
39+
.joinToString("&")
3040

3141
return MessageDigest.getInstance("SHA-1")
32-
.digest((params.filter { it.isNotBlank() }.sorted().joinToString("&") + apiSecret).toByteArray(Charsets.UTF_8))
42+
.digest((queryString + apiSecret).toByteArray(Charsets.UTF_8))
3343
.toHex()
3444
}
3545

uploader/src/test/kotlin/com/cloudinary/UploaderTest.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import java.net.URL
3535
import java.text.SimpleDateFormat
3636
import java.util.*
3737
import kotlin.test.Test
38+
import kotlin.test.assertNotEquals
3839
import kotlin.test.assertNotNull
3940
import kotlin.test.assertNull
4041
import kotlin.test.assertTrue
@@ -1045,13 +1046,45 @@ class UploaderTest(networkLayer: NetworkLayer) {
10451046
assertNotNull(result.playbackUrl)
10461047
}
10471048

1049+
@Test
1050+
fun testSignatureWithEscapingCharacters() {
1051+
val cloudName = "dn6ot3ged"
1052+
val apiSecret = "hdcixPpR2iKERPwqvH6sHdK9cyac"
1053+
1054+
val paramsWithAmpersand = mapOf(
1055+
"cloud_name" to cloudName,
1056+
"timestamp" to 1568810420,
1057+
"notification_url" to "https://fake.com/callback?a=1&tags=hello,world"
1058+
)
1059+
1060+
val signatureWithAmpersand = apiSignRequest(paramsWithAmpersand, apiSecret, cloudinary.config.urlConfig.signatureVersion)
1061+
1062+
val paramsSmuggled = mapOf(
1063+
"cloud_name" to cloudName,
1064+
"timestamp" to 1568810420,
1065+
"notification_url" to "https://fake.com/callback?a=1",
1066+
"tags" to "hello,world"
1067+
)
1068+
1069+
val signatureSmuggled = apiSignRequest(paramsSmuggled, apiSecret, cloudinary.config.urlConfig.signatureVersion)
1070+
1071+
assertNotEquals(signatureWithAmpersand, signatureSmuggled,
1072+
"Signatures should be different to prevent parameter smuggling")
1073+
1074+
val expectedSignature = "4fdf465dd89451cc1ed8ec5b3e314e8a51695704"
1075+
assertEquals(expectedSignature, signatureWithAmpersand)
1076+
1077+
val expectedSmuggledSignature = "7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9"
1078+
assertEquals(expectedSmuggledSignature, signatureSmuggled)
1079+
}
1080+
10481081
private fun validateSignature(result: UploadResult) {
10491082
val toSign: MutableMap<String, Any> = HashMap()
10501083
toSign["public_id"] = result.publicId!!
10511084
toSign["version"] = result.version.toString()
10521085

10531086
val expectedSignature: String =
1054-
apiSignRequest(toSign, cloudinary.config.apiSecret!!)
1087+
apiSignRequest(toSign, cloudinary.config.apiSecret!!, cloudinary.config.urlConfig.signatureVersion)
10551088
assertEquals(result.signature, expectedSignature)
10561089
}
10571090

url-gen/src/main/kotlin/com/cloudinary/config/UrlConfig.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal const val DEFAULT_SECURE_CDN_SUBDOMAIN = false
1010
internal const val DEFAULT_LONG_URL_SIGNATURE = false
1111
internal const val DEFAULT_SIGN_URL = false
1212
internal const val DEFAULT_SIGNATURE_ALGORITHM = "SHA-1"
13+
internal const val DEFAULT_SIGNATURE_VERSION = 2
1314
internal const val DEFAULT_ANALYTICS = true
1415

1516
const val SIGN_URL = "sign_url"
@@ -25,6 +26,7 @@ const val USE_ROOT_PATH = "use_root_path"
2526
const val CNAME = "cname"
2627
const val SECURE = "secure"
2728
const val SIGNATURE_ALGORITHM = "signature_algorithm"
29+
const val SIGNATURE_VERSION = "signature_version"
2830
const val ANALYTICS = "analytics"
2931

3032
interface IUrlConfig {
@@ -41,6 +43,7 @@ interface IUrlConfig {
4143
val secure: Boolean
4244
val forceVersion: Boolean
4345
val signatureAlgorithm: String?
46+
val signatureVersion: Int
4447
val analytics: Boolean
4548
}
4649

@@ -61,6 +64,7 @@ data class UrlConfig(
6164
override val secure: Boolean = DEFAULT_SECURE,
6265
override val forceVersion: Boolean = DEFAULT_FORCE_VERSION,
6366
override val signatureAlgorithm: String = DEFAULT_SIGNATURE_ALGORITHM,
67+
override val signatureVersion: Int = DEFAULT_SIGNATURE_VERSION,
6468
override val analytics: Boolean = DEFAULT_ANALYTICS
6569
) : IUrlConfig {
6670
constructor(params: Map<String, Any>) : this(
@@ -76,6 +80,7 @@ data class UrlConfig(
7680
secure = params.getBoolean(SECURE) ?: DEFAULT_SECURE,
7781
forceVersion = params.getBoolean(FORCE_VERSION) ?: DEFAULT_FORCE_VERSION,
7882
signatureAlgorithm = params[SIGNATURE_ALGORITHM]?.toString() ?: DEFAULT_SIGNATURE_ALGORITHM,
83+
signatureVersion = params.getInt(SIGNATURE_VERSION) ?: DEFAULT_SIGNATURE_VERSION,
7984
analytics = params.getBoolean(ANALYTICS) ?: DEFAULT_ANALYTICS
8085
)
8186
}

0 commit comments

Comments
 (0)