Skip to content

Commit 4df6b07

Browse files
authored
fix: correct signing for query parameters containing '+' and '%' (#654)
1 parent eb6e435 commit 4df6b07

File tree

3 files changed

+34
-9
lines changed

3 files changed

+34
-9
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "922b32ef-f194-4e5f-9ff2-9eca98246755",
3+
"type": "bugfix",
4+
"description": "Fix bugs with signing for query parameters containing '+' and '%'",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#619"
7+
]
8+
}

runtime/utils/common/src/aws/smithy/kotlin/runtime/util/text/Text.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,37 +170,40 @@ public fun String.splitAsQueryString(): Map<String, List<String>> {
170170
* Decode a URL's query string, resolving percent-encoding (e.g., "%3B" → ";").
171171
*/
172172
@InternalApi
173-
public fun String.urlDecodeComponent(): String {
173+
public fun String.urlDecodeComponent(formUrlDecode: Boolean = false): String {
174174
val orig = this
175175
return buildString(orig.length) {
176176
var byteBuffer: ByteArray? = null // Do not initialize unless needed
177177
var i = 0
178178
var c: Char
179179
while (i < orig.length) {
180180
c = orig[i]
181-
when (c) {
182-
'+' -> {
181+
when {
182+
c == '+' && formUrlDecode -> {
183183
append(' ')
184184
i++
185185
}
186186

187-
'%' -> {
187+
c == '%' -> {
188188
if (byteBuffer == null) {
189189
byteBuffer = ByteArray((orig.length - i) / 3) // Max remaining percent-encoded bytes
190190
}
191191

192192
var byteCount = 0
193193
while ((i + 2) < orig.length && c == '%') {
194-
val byte = orig.substring(i + 1, i + 3).toInt(radix = 16).toByte()
194+
val byte = orig.substring(i + 1, i + 3).toIntOrNull(radix = 16)?.toByte() ?: break
195195
byteBuffer[byteCount++] = byte
196196

197197
i += 3
198198
if (i < orig.length) c = orig[i]
199199
}
200200

201-
require(i == orig.length || c != '%') { "Incomplete escape pattern at end of string" }
202-
203201
append(byteBuffer.decodeToString(endIndex = byteCount))
202+
203+
if (i != orig.length && c == '%') {
204+
append(c)
205+
i++
206+
}
204207
}
205208

206209
else -> {
@@ -213,5 +216,5 @@ public fun String.urlDecodeComponent(): String {
213216
}
214217

215218
@InternalApi
216-
public fun String.urlReencodeComponent(formUrlEncode: Boolean = false): String =
217-
urlDecodeComponent().urlEncodeComponent(formUrlEncode)
219+
public fun String.urlReencodeComponent(formUrlDecode: Boolean = false, formUrlEncode: Boolean = false): String =
220+
urlDecodeComponent(formUrlDecode).urlEncodeComponent(formUrlEncode)

runtime/utils/common/test/aws/smithy/kotlin/runtime/util/text/TextTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,22 @@ class TextTest {
148148

149149
@Test
150150
fun decodeUrlComponent() {
151+
val component = "a%3Bb+c%7Ed%20e%2Bf+g%3D%E1%88%B4"
152+
val expected = "a;b+c~d e+f+g=ሴ"
153+
assertEquals(expected, component.urlDecodeComponent())
154+
}
155+
156+
@Test
157+
fun decodeUrlComponentWithFormUrl() {
151158
val component = "a%3Bb+c%7Ed%20e%2Bf+g%3D%E1%88%B4"
152159
val expected = "a;b c~d e+f g=ሴ"
160+
assertEquals(expected, component.urlDecodeComponent(true))
161+
}
162+
163+
@Test
164+
fun decodeUrlComponentInvalidSequence() {
165+
val component = "%20%&'%%%f"
166+
val expected = " %&'%%%f" // Only the %20 was valid
153167
assertEquals(expected, component.urlDecodeComponent())
154168
}
155169

0 commit comments

Comments
 (0)