expo-updates 57.0.13 → 57.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/updates/UpdatesUtils.kt +11 -0
- package/android/src/main/java/expo/modules/updates/db/Reaper.kt +16 -1
- package/android/src/main/java/expo/modules/updates/loader/FileDownloader.kt +6 -0
- package/android/src/main/java/expo/modules/updates/manifest/ExpoUpdatesUpdate.kt +35 -18
- package/ios/EXUpdates/AppLoader/RemoteAppLoader.swift +7 -0
- package/ios/EXUpdates/Database/UpdatesReaper.swift +4 -0
- package/ios/EXUpdates/Update/Update.swift +15 -1
- package/ios/EXUpdates/UpdatesError.swift +3 -0
- package/ios/EXUpdates/UpdatesUtils.swift +17 -0
- package/ios/EXUpdates.podspec +1 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.0.14 — 2026-08-14
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- Reject updates whose asset key or file extension contains a path separator, which previously let a manifest write and delete files outside the updates directory. ([#48762](https://github.com/expo/expo/pull/48762), [#48763](https://github.com/expo/expo/pull/48763) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
|
|
19
|
+
### 💡 Others
|
|
20
|
+
|
|
21
|
+
- [iOS] Link `libc++` in the test spec so the unit test bundle resolves the C++ symbols it pulls from `ExpoModulesCore`. ([#48762](https://github.com/expo/expo/pull/48762) by [@alanjhughes](https://github.com/alanjhughes))
|
|
22
|
+
|
|
13
23
|
## 57.0.13 — 2026-08-10
|
|
14
24
|
|
|
15
25
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -42,7 +42,7 @@ expoModule {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
group = 'host.exp.exponent'
|
|
45
|
-
version = '57.0.
|
|
45
|
+
version = '57.0.14'
|
|
46
46
|
|
|
47
47
|
// Utility method to derive boolean values from the environment or from Java properties,
|
|
48
48
|
// and return them as strings to be used in BuildConfig fields
|
|
@@ -89,7 +89,7 @@ android {
|
|
|
89
89
|
namespace "expo.modules.updates"
|
|
90
90
|
defaultConfig {
|
|
91
91
|
versionCode 31
|
|
92
|
-
versionName '57.0.
|
|
92
|
+
versionName '57.0.14'
|
|
93
93
|
consumerProguardFiles("proguard-rules.pro")
|
|
94
94
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
95
95
|
|
|
@@ -166,6 +166,17 @@ object UpdatesUtils {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Asset filenames are built from manifest-controlled values, so a filename holding a path
|
|
171
|
+
* separator or a `..` component would resolve outside the updates directory.
|
|
172
|
+
*/
|
|
173
|
+
fun isSafeFilename(filename: String): Boolean {
|
|
174
|
+
return filename.isNotEmpty() &&
|
|
175
|
+
filename != "." &&
|
|
176
|
+
filename != ".." &&
|
|
177
|
+
filename.none { it == '/' || it == '\\' || it == '\u0000' }
|
|
178
|
+
}
|
|
179
|
+
|
|
169
180
|
fun shouldCheckForUpdateOnLaunch(
|
|
170
181
|
updatesConfiguration: UpdatesConfiguration,
|
|
171
182
|
logger: UpdatesLogger,
|
|
@@ -2,10 +2,12 @@ package expo.modules.updates.db
|
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import expo.modules.updates.UpdatesConfiguration
|
|
5
|
+
import expo.modules.updates.UpdatesUtils
|
|
5
6
|
import expo.modules.updates.db.entity.AssetEntity
|
|
6
7
|
import expo.modules.updates.db.entity.UpdateEntity
|
|
7
8
|
import expo.modules.updates.manifest.ManifestMetadata
|
|
8
9
|
import expo.modules.updates.selectionpolicy.SelectionPolicy
|
|
10
|
+
import expo.modules.updates.utils.AndroidResourceAssetUtils
|
|
9
11
|
import java.io.File
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -47,7 +49,20 @@ object Reaper {
|
|
|
47
49
|
)
|
|
48
50
|
continue
|
|
49
51
|
}
|
|
50
|
-
val
|
|
52
|
+
val relativePath = asset.relativePath
|
|
53
|
+
// Embedded assets are served from the APK, so there is no file here to delete.
|
|
54
|
+
if (relativePath == null || AndroidResourceAssetUtils.isAndroidResourceAsset(relativePath)) {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
// A row written before asset filenames were validated may point outside the updates directory.
|
|
58
|
+
if (!UpdatesUtils.isSafeFilename(relativePath)) {
|
|
59
|
+
Log.e(
|
|
60
|
+
TAG,
|
|
61
|
+
"Refusing to delete asset with URL " + asset.url + " at unsafe path " + relativePath
|
|
62
|
+
)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
val path = File(updatesDirectory, relativePath)
|
|
51
66
|
try {
|
|
52
67
|
if (path.exists() && !path.delete()) {
|
|
53
68
|
Log.e(TAG, "Failed to delete asset with URL " + asset.url + " at path " + path.toString())
|
|
@@ -687,6 +687,12 @@ class FileDownloader(
|
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
val filename = UpdatesUtils.createFilenameForAsset(asset)
|
|
690
|
+
if (!UpdatesUtils.isSafeFilename(filename)) {
|
|
691
|
+
val message = "Failed to download asset ${asset.key}"
|
|
692
|
+
val error = IOException("Asset filename \"$filename\" would resolve outside the updates directory")
|
|
693
|
+
logger.error(message, error, UpdatesErrorCode.AssetsFailedToLoad)
|
|
694
|
+
throw IOException(message, error)
|
|
695
|
+
}
|
|
690
696
|
val path = File(destinationDirectory, filename)
|
|
691
697
|
|
|
692
698
|
if (path.exists()) {
|
|
@@ -14,6 +14,7 @@ import expo.modules.updates.db.enums.UpdateStatus
|
|
|
14
14
|
import org.json.JSONArray
|
|
15
15
|
import org.json.JSONException
|
|
16
16
|
import org.json.JSONObject
|
|
17
|
+
import java.io.IOException
|
|
17
18
|
import java.text.ParseException
|
|
18
19
|
import java.util.*
|
|
19
20
|
|
|
@@ -95,27 +96,43 @@ class ExpoUpdatesUpdate private constructor(
|
|
|
95
96
|
companion object {
|
|
96
97
|
private val TAG = Update::class.java.simpleName
|
|
97
98
|
|
|
98
|
-
@Throws(
|
|
99
|
+
@Throws(Exception::class)
|
|
99
100
|
fun fromExpoUpdatesManifest(
|
|
100
101
|
manifest: ExpoUpdatesManifest,
|
|
101
102
|
extensions: JSONObject?,
|
|
102
103
|
configuration: UpdatesConfiguration
|
|
103
|
-
): ExpoUpdatesUpdate
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
): ExpoUpdatesUpdate {
|
|
105
|
+
val update = ExpoUpdatesUpdate(
|
|
106
|
+
manifest,
|
|
107
|
+
id = UUID.fromString(manifest.getID()),
|
|
108
|
+
configuration.scopeKey,
|
|
109
|
+
commitTime = try {
|
|
110
|
+
UpdatesUtils.parseDateString(manifest.getCreatedAt())
|
|
111
|
+
} catch (e: ParseException) {
|
|
112
|
+
Log.e(TAG, "Could not parse manifest createdAt string; falling back to current time", e)
|
|
113
|
+
Date()
|
|
114
|
+
},
|
|
115
|
+
runtimeVersion = manifest.getRuntimeVersion(),
|
|
116
|
+
launchAsset = manifest.getLaunchAsset(),
|
|
117
|
+
assets = manifest.getAssets(),
|
|
118
|
+
extensions = extensions,
|
|
119
|
+
url = configuration.updateUrl,
|
|
120
|
+
requestHeaders = configuration.requestHeaders
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
update.assetEntityList.forEach { asset ->
|
|
124
|
+
val filename = UpdatesUtils.createFilenameForAsset(asset)
|
|
125
|
+
if (!UpdatesUtils.isSafeFilename(filename)) {
|
|
126
|
+
throw IOException(
|
|
127
|
+
"Update ${manifest.getID()} could not be loaded because the asset filename " +
|
|
128
|
+
"\"$filename\" is not a valid filename. Assets are stored under their key and file " +
|
|
129
|
+
"extension, so neither may contain a path separator. Check the server that produced " +
|
|
130
|
+
"this manifest."
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return update
|
|
136
|
+
}
|
|
120
137
|
}
|
|
121
138
|
}
|
|
@@ -105,6 +105,13 @@ public final class RemoteAppLoader: AppLoader {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
|
|
108
|
+
guard UpdatesUtils.isSafeFilename(asset.filename) else {
|
|
109
|
+
self.handleAssetDownload(
|
|
110
|
+
withError: UpdatesError.remoteAppLoaderUnsafeAssetFilename(filename: asset.filename),
|
|
111
|
+
asset: asset
|
|
112
|
+
)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
108
115
|
let urlOnDisk = self.directory.appendingPathComponent(asset.filename)
|
|
109
116
|
|
|
110
117
|
let progressBlock = { [weak self] fractionCompleted in
|
|
@@ -71,6 +71,10 @@ public final class UpdatesReaper: NSObject {
|
|
|
71
71
|
|
|
72
72
|
let beginDeleteAssets = Date()
|
|
73
73
|
for asset in assetsForDeletion {
|
|
74
|
+
guard UpdatesUtils.isSafeFilename(asset.filename) else {
|
|
75
|
+
logger.warn(message: "Refusing to delete asset at unsafe path \(asset.filename)")
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
74
78
|
let localUrl = directory.appendingPathComponent(asset.filename)
|
|
75
79
|
if FileManager.default.fileExists(atPath: localUrl.path) {
|
|
76
80
|
do {
|
|
@@ -77,6 +77,7 @@ public enum UpdateStatus: Int {
|
|
|
77
77
|
public enum UpdateError: Error, Sendable, LocalizedError {
|
|
78
78
|
case invalidExpoProtocolVersion(protocolVersion: Int)
|
|
79
79
|
case legacyManifestInstantiationInvalid
|
|
80
|
+
case unsafeAssetFilename(updateId: String, filename: String)
|
|
80
81
|
|
|
81
82
|
public var errorDescription: String? {
|
|
82
83
|
switch self {
|
|
@@ -84,6 +85,10 @@ public enum UpdateError: Error, Sendable, LocalizedError {
|
|
|
84
85
|
return "Invalid Expo Updates protocol version: \(protocolVersion)"
|
|
85
86
|
case .legacyManifestInstantiationInvalid:
|
|
86
87
|
return "This version of expo-updates can no longer load legacy manifests"
|
|
88
|
+
case let .unsafeAssetFilename(updateId, filename):
|
|
89
|
+
return "Update \(updateId) could not be loaded because the asset filename \"\(filename)\" " +
|
|
90
|
+
"is not a valid filename. Assets are stored under their key and file extension, so neither " +
|
|
91
|
+
"may contain a path separator. Check the server that produced this manifest."
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -157,12 +162,21 @@ public class Update: NSObject {
|
|
|
157
162
|
}
|
|
158
163
|
switch protocolVersion {
|
|
159
164
|
case 0, 1:
|
|
160
|
-
|
|
165
|
+
let update = ExpoUpdatesUpdate.update(
|
|
161
166
|
withExpoUpdatesManifest: ExpoUpdatesManifest(rawManifestJSON: withManifest),
|
|
162
167
|
extensions: extensions,
|
|
163
168
|
config: config,
|
|
164
169
|
database: database
|
|
165
170
|
)
|
|
171
|
+
try update.assets()?.forEach { asset in
|
|
172
|
+
guard UpdatesUtils.isSafeFilename(asset.filename) else {
|
|
173
|
+
throw UpdateError.unsafeAssetFilename(
|
|
174
|
+
updateId: update.updateId.uuidString,
|
|
175
|
+
filename: asset.filename
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return update
|
|
166
180
|
default:
|
|
167
181
|
throw UpdateError.invalidExpoProtocolVersion(protocolVersion: protocolVersion)
|
|
168
182
|
}
|
|
@@ -26,6 +26,7 @@ public enum UpdatesError: Error, Sendable, LocalizedError {
|
|
|
26
26
|
case fileDownloaderFailedUpdateIDsFailure(cause: Error)
|
|
27
27
|
case fileDownloaderUnknownError(cause: Error)
|
|
28
28
|
case remoteAppLoaderAssetMissingUrl
|
|
29
|
+
case remoteAppLoaderUnsafeAssetFilename(filename: String)
|
|
29
30
|
case remoteAppLoaderHeaderDataError(cause: Error)
|
|
30
31
|
case remoteAppLoaderUnknownError(cause: Error)
|
|
31
32
|
case appLoaderFailedToLoadAllAssets
|
|
@@ -92,6 +93,8 @@ public enum UpdatesError: Error, Sendable, LocalizedError {
|
|
|
92
93
|
return "Unknown error: \(cause.localizedDescription)"
|
|
93
94
|
case .remoteAppLoaderAssetMissingUrl:
|
|
94
95
|
return "Failed to download asset with no URL provided"
|
|
96
|
+
case let .remoteAppLoaderUnsafeAssetFilename(filename):
|
|
97
|
+
return "Failed to download asset: filename \"\(filename)\" would resolve outside the updates directory"
|
|
95
98
|
case let .remoteAppLoaderHeaderDataError(cause):
|
|
96
99
|
return "Error persisting header data to disk: \(cause.localizedDescription)"
|
|
97
100
|
case .appLoaderFailedToLoadAllAssets:
|
|
@@ -75,6 +75,23 @@ public final class UpdatesUtils: NSObject {
|
|
|
75
75
|
return updatesDirectory
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Asset filenames are built from manifest-controlled values, so a filename holding a path
|
|
80
|
+
* separator or a `..` component would resolve outside the updates directory.
|
|
81
|
+
*
|
|
82
|
+
* Matching is done on unicode scalars because `String.contains` compares grapheme clusters, and
|
|
83
|
+
* a separator followed by a combining mark forms one cluster that does not equal the separator.
|
|
84
|
+
* The filesystem still sees the separator byte.
|
|
85
|
+
*/
|
|
86
|
+
public static func isSafeFilename(_ filename: String) -> Bool {
|
|
87
|
+
return !filename.isEmpty &&
|
|
88
|
+
filename != "." &&
|
|
89
|
+
filename != ".." &&
|
|
90
|
+
!filename.unicodeScalars.contains("/") &&
|
|
91
|
+
!filename.unicodeScalars.contains("\\") &&
|
|
92
|
+
!filename.unicodeScalars.contains("\0")
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
// MARK: - Internal methods
|
|
79
96
|
|
|
80
97
|
public static func defaultNativeStateMachineContextJson() -> [String: Any?] {
|
package/ios/EXUpdates.podspec
CHANGED
|
@@ -141,6 +141,7 @@ Pod::Spec.new do |s|
|
|
|
141
141
|
test_spec.dependency 'ExpoModulesTestCore'
|
|
142
142
|
|
|
143
143
|
test_spec.pod_target_xcconfig = {
|
|
144
|
+
'OTHER_LDFLAGS' => '$(inherited) -lc++',
|
|
144
145
|
'USER_HEADER_SEARCH_PATHS' => '"${CONFIGURATION_TEMP_DIR}/EXUpdates.build/DerivedSources"',
|
|
145
146
|
'GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS' => 'YES',
|
|
146
147
|
'GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS' => 'YES',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-updates",
|
|
3
|
-
"version": "57.0.
|
|
3
|
+
"version": "57.0.14",
|
|
4
4
|
"description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"picomatch": "^4.0.4",
|
|
61
61
|
"ts-node": "^10.9.2",
|
|
62
62
|
"xstate": "^4.37.2",
|
|
63
|
-
"expo": "57.0.
|
|
64
|
-
"expo-dev-client": "57.0.
|
|
65
|
-
"expo
|
|
66
|
-
"
|
|
63
|
+
"@expo/metro-config": "57.0.8",
|
|
64
|
+
"expo-dev-client": "57.0.12",
|
|
65
|
+
"expo": "57.0.13",
|
|
66
|
+
"expo-module-scripts": "56.0.3"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"expo": "*",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"./scripts/with-node.sh"
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "125b8d4472432db3dc4ae047730432d7bd5bd30c",
|
|
86
86
|
"scripts": {
|
|
87
87
|
"build": "expo-module build",
|
|
88
88
|
"clean": "expo-module clean",
|