Skip to content

Commit 15501ec

Browse files
[shared_preferences] Limit Android decoding (#8187)
When decoding stored preferences for `List<String>` support, only allow lists and strings to avoid potentially instantiating other object types.
1 parent a4ac811 commit 15501ec

File tree

6 files changed

+70
-19
lines changed

6 files changed

+70
-19
lines changed

packages/shared_preferences/shared_preferences_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.3.4
2+
3+
* Restrict types when decoding preferences.
4+
15
## 2.3.3
26

37
* Updates Java compatibility version to 11.

packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/LegacySharedPreferencesPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ static class ListEncoder implements SharedPreferencesListEncoder {
197197
public @NonNull List<String> decode(@NonNull String listString) throws RuntimeException {
198198
try {
199199
ObjectInputStream stream =
200-
new ObjectInputStream(new ByteArrayInputStream(Base64.decode(listString, 0)));
200+
new StringListObjectInputStream(new ByteArrayInputStream(Base64.decode(listString, 0)));
201201
return (List<String>) stream.readObject();
202202
} catch (IOException | ClassNotFoundException e) {
203203
throw new RuntimeException(e);

packages/shared_preferences/shared_preferences_android/android/src/main/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.kt

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
2020
import io.flutter.plugin.common.BinaryMessenger
2121
import java.io.ByteArrayInputStream
2222
import java.io.ByteArrayOutputStream
23-
import java.io.ObjectInputStream
2423
import java.io.ObjectOutputStream
2524
import kotlinx.coroutines.flow.Flow
2625
import kotlinx.coroutines.flow.firstOrNull
@@ -250,25 +249,17 @@ class SharedPreferencesPlugin() : FlutterPlugin, SharedPreferencesAsyncApi {
250249
/** Class that provides tools for encoding and decoding List<String> to String and back. */
251250
class ListEncoder : SharedPreferencesListEncoder {
252251
override fun encode(list: List<String>): String {
253-
try {
254-
val byteStream = ByteArrayOutputStream()
255-
val stream = ObjectOutputStream(byteStream)
256-
stream.writeObject(list)
257-
stream.flush()
258-
return Base64.encodeToString(byteStream.toByteArray(), 0)
259-
} catch (e: RuntimeException) {
260-
throw RuntimeException(e)
261-
}
252+
val byteStream = ByteArrayOutputStream()
253+
val stream = ObjectOutputStream(byteStream)
254+
stream.writeObject(list)
255+
stream.flush()
256+
return Base64.encodeToString(byteStream.toByteArray(), 0)
262257
}
263258

264259
override fun decode(listString: String): List<String> {
265-
try {
266-
val byteArray = Base64.decode(listString, 0)
267-
val stream = ObjectInputStream(ByteArrayInputStream(byteArray))
268-
return (stream.readObject() as List<*>).filterIsInstance<String>()
269-
} catch (e: RuntimeException) {
270-
throw RuntimeException(e)
271-
}
260+
val byteArray = Base64.decode(listString, 0)
261+
val stream = StringListObjectInputStream(ByteArrayInputStream(byteArray))
262+
return (stream.readObject() as List<*>).filterIsInstance<String>()
272263
}
273264
}
274265
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.sharedpreferences
6+
7+
import java.io.IOException
8+
import java.io.InputStream
9+
import java.io.ObjectInputStream
10+
import java.io.ObjectStreamClass
11+
12+
/**
13+
* An ObjectInputStream that only allows string lists, to prevent injected prefs from instantiating
14+
* arbitrary objects.
15+
*/
16+
class StringListObjectInputStream(input: InputStream) : ObjectInputStream(input) {
17+
@Throws(ClassNotFoundException::class, IOException::class)
18+
override fun resolveClass(desc: ObjectStreamClass?): Class<*>? {
19+
val allowList =
20+
setOf(
21+
"java.util.Arrays\$ArrayList",
22+
"java.util.ArrayList",
23+
"java.lang.String",
24+
"[Ljava.lang.String;")
25+
val name = desc?.name
26+
if (name != null && !allowList.contains(name)) {
27+
throw ClassNotFoundException(desc.name)
28+
}
29+
return super.resolveClass(desc)
30+
}
31+
}

packages/shared_preferences/shared_preferences_android/android/src/test/kotlin/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
package io.flutter.plugins.sharedpreferences
66

77
import android.content.Context
8+
import android.util.Base64
89
import androidx.test.core.app.ApplicationProvider
910
import androidx.test.ext.junit.runners.AndroidJUnit4
1011
import io.flutter.embedding.engine.plugins.FlutterPlugin
1112
import io.flutter.plugin.common.BinaryMessenger
1213
import io.mockk.every
1314
import io.mockk.mockk
15+
import java.io.ByteArrayOutputStream
16+
import java.io.ObjectOutputStream
1417
import org.junit.Assert
18+
import org.junit.Assert.assertThrows
1519
import org.junit.Test
1620
import org.junit.runner.RunWith
1721

@@ -48,6 +52,7 @@ internal class SharedPreferencesTest {
4852
every { flutterPluginBinding.binaryMessenger } returns binaryMessenger
4953
every { flutterPluginBinding.applicationContext } returns testContext
5054
plugin.onAttachedToEngine(flutterPluginBinding)
55+
plugin.clear(null, emptyOptions)
5156
return plugin
5257
}
5358

@@ -171,4 +176,24 @@ internal class SharedPreferencesTest {
171176
Assert.assertNull(all[doubleKey])
172177
Assert.assertNull(all[listKey])
173178
}
179+
180+
@Test
181+
fun testUnexpectedClassDecodeThrows() {
182+
// Only String should be allowed in an encoded list.
183+
val badList = listOf(1, 2, 3)
184+
// Replicate the behavior of ListEncoder.encode, but with a non-List<String> list.
185+
val byteStream = ByteArrayOutputStream()
186+
val stream = ObjectOutputStream(byteStream)
187+
stream.writeObject(badList)
188+
stream.flush()
189+
val badPref = LIST_PREFIX + Base64.encodeToString(byteStream.toByteArray(), 0)
190+
191+
val plugin = pluginSetup()
192+
val badListKey = "badList"
193+
// Inject the bad pref as a string, as that is how string lists are stored internally.
194+
plugin.setString(badListKey, badPref, emptyOptions)
195+
assertThrows(ClassNotFoundException::class.java) {
196+
plugin.getStringList(badListKey, emptyOptions)
197+
}
198+
}
174199
}

packages/shared_preferences/shared_preferences_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: shared_preferences_android
22
description: Android implementation of the shared_preferences plugin
33
repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22
5-
version: 2.3.3
5+
version: 2.3.4
66

77
environment:
88
sdk: ^3.5.0

0 commit comments

Comments
 (0)