@capgo/capacitor-updater 8.47.2 → 8.47.3
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/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +36 -14
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +21 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +59 -17
- package/package.json +1 -1
|
@@ -120,7 +120,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
120
120
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
121
121
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
122
122
|
|
|
123
|
-
private final String pluginVersion = "8.47.
|
|
123
|
+
private final String pluginVersion = "8.47.3";
|
|
124
124
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
125
125
|
|
|
126
126
|
private SharedPreferences.Editor editor;
|
|
@@ -193,6 +193,30 @@ public class CapgoUpdater {
|
|
|
193
193
|
this.cachedKeyId = CryptoCipher.calcKeyId(publicKey);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
static File resolvePathInsideDirectory(final File baseDirectory, final String relativePath) throws IOException {
|
|
197
|
+
if (relativePath == null || relativePath.isEmpty()) {
|
|
198
|
+
throw new IOException("Invalid empty path");
|
|
199
|
+
}
|
|
200
|
+
if (relativePath.contains("\\") || relativePath.indexOf('\0') >= 0) {
|
|
201
|
+
throw new IOException("Invalid path separator");
|
|
202
|
+
}
|
|
203
|
+
if (new File(relativePath).isAbsolute()) {
|
|
204
|
+
throw new IOException("Absolute paths are not allowed");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
final File canonicalBase = baseDirectory.getCanonicalFile();
|
|
208
|
+
final File canonicalTarget = new File(canonicalBase, relativePath).getCanonicalFile();
|
|
209
|
+
final String basePath = canonicalBase.getPath();
|
|
210
|
+
final String targetPath = canonicalTarget.getPath();
|
|
211
|
+
final String normalizedBasePath = basePath.endsWith(File.separator) ? basePath : basePath + File.separator;
|
|
212
|
+
|
|
213
|
+
if (!targetPath.equals(basePath) && !targetPath.startsWith(normalizedBasePath)) {
|
|
214
|
+
throw new IOException("Path escapes base directory: " + relativePath);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return canonicalTarget;
|
|
218
|
+
}
|
|
219
|
+
|
|
196
220
|
public String getKeyId() {
|
|
197
221
|
return this.cachedKeyId;
|
|
198
222
|
}
|
|
@@ -213,23 +237,21 @@ public class CapgoUpdater {
|
|
|
213
237
|
|
|
214
238
|
ZipEntry entry;
|
|
215
239
|
while ((entry = zis.getNextEntry()) != null) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
240
|
+
final File file;
|
|
241
|
+
try {
|
|
242
|
+
file = resolvePathInsideDirectory(targetDirectory, entry.getName());
|
|
243
|
+
} catch (IOException e) {
|
|
244
|
+
if (entry.getName().contains("\\")) {
|
|
245
|
+
logger.error("Unzip failed: Windows path not supported");
|
|
246
|
+
logger.debug("Invalid path: " + entry.getName());
|
|
247
|
+
this.sendStats("windows_path_fail");
|
|
248
|
+
} else {
|
|
249
|
+
this.sendStats("canonical_path_fail");
|
|
250
|
+
}
|
|
251
|
+
throw e;
|
|
220
252
|
}
|
|
221
|
-
final File file = new File(targetDirectory, entry.getName());
|
|
222
|
-
final String canonicalPath = file.getCanonicalPath();
|
|
223
|
-
final String canonicalDir = targetDirectory.getCanonicalPath();
|
|
224
253
|
final File dir = entry.isDirectory() ? file : file.getParentFile();
|
|
225
254
|
|
|
226
|
-
if (!canonicalPath.startsWith(canonicalDir)) {
|
|
227
|
-
this.sendStats("canonical_path_fail");
|
|
228
|
-
throw new FileNotFoundException(
|
|
229
|
-
"SecurityException, Failed to ensure directory is the start path : " + canonicalDir + " of " + canonicalPath
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
255
|
assert dir != null;
|
|
234
256
|
if (!dir.isDirectory() && !dir.mkdirs()) {
|
|
235
257
|
this.sendStats("directory_path_fail");
|
|
@@ -168,6 +168,16 @@ public class DownloadService extends Worker {
|
|
|
168
168
|
return Result.success(output);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
static File resolveManifestTargetFile(final File destFolder, final String fileName) throws IOException {
|
|
172
|
+
final boolean isBrotli = fileName.endsWith(".br");
|
|
173
|
+
final String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
174
|
+
return CapgoUpdater.resolvePathInsideDirectory(destFolder, targetFileName);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static File resolveManifestBuiltinFile(final File builtinFolder, final String fileName) throws IOException {
|
|
178
|
+
return CapgoUpdater.resolvePathInsideDirectory(builtinFolder, fileName);
|
|
179
|
+
}
|
|
180
|
+
|
|
171
181
|
private String getInputString(String key, String fallback) {
|
|
172
182
|
String value = getInputData().getString(key);
|
|
173
183
|
return value != null ? value : fallback;
|
|
@@ -338,11 +348,20 @@ public class DownloadService extends Worker {
|
|
|
338
348
|
boolean isBrotli = fileName.endsWith(".br");
|
|
339
349
|
String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
340
350
|
|
|
341
|
-
File targetFile
|
|
351
|
+
File targetFile;
|
|
352
|
+
File builtinFile;
|
|
353
|
+
try {
|
|
354
|
+
targetFile = resolveManifestTargetFile(destFolder, fileName);
|
|
355
|
+
builtinFile = resolveManifestBuiltinFile(builtinFolder, fileName);
|
|
356
|
+
} catch (IOException e) {
|
|
357
|
+
logger.error("Invalid manifest file path: " + fileName);
|
|
358
|
+
sendStatsAsync("manifest_path_fail", version + ":" + fileName);
|
|
359
|
+
hasError.set(true);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
342
362
|
String cacheBaseName = new File(isBrotli ? targetFileName : fileName).getName();
|
|
343
363
|
File cacheFile = new File(cacheFolder, finalFileHash + "_" + cacheBaseName);
|
|
344
364
|
final File legacyCacheFile = isBrotli ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
|
|
345
|
-
File builtinFile = new File(builtinFolder, fileName);
|
|
346
365
|
|
|
347
366
|
// Ensure parent directories of the target file exist
|
|
348
367
|
if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
|
|
@@ -79,7 +79,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
79
79
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
80
80
|
]
|
|
81
81
|
public var implementation = CapgoUpdater()
|
|
82
|
-
private let pluginVersion: String = "8.47.
|
|
82
|
+
private let pluginVersion: String = "8.47.3"
|
|
83
83
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
84
84
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
85
85
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -97,6 +97,43 @@ import UIKit
|
|
|
97
97
|
let timedOut: Bool
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
enum SecurePathError: Error {
|
|
101
|
+
case emptyPath
|
|
102
|
+
case windowsPath
|
|
103
|
+
case absolutePath
|
|
104
|
+
case pathTraversal
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static func resolvePathInsideDirectory(baseDirectory: URL, relativePath: String) throws -> URL {
|
|
108
|
+
if relativePath.isEmpty {
|
|
109
|
+
throw SecurePathError.emptyPath
|
|
110
|
+
}
|
|
111
|
+
if relativePath.contains("\\") || relativePath.contains("\0") {
|
|
112
|
+
throw SecurePathError.windowsPath
|
|
113
|
+
}
|
|
114
|
+
if (relativePath as NSString).isAbsolutePath {
|
|
115
|
+
throw SecurePathError.absolutePath
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let canonicalBase = baseDirectory.standardizedFileURL
|
|
119
|
+
let canonicalBasePath = canonicalBase.path
|
|
120
|
+
let normalizedBasePath = canonicalBasePath.hasSuffix("/") ? canonicalBasePath : "\(canonicalBasePath)/"
|
|
121
|
+
let canonicalTarget = canonicalBase.appendingPathComponent(relativePath).standardizedFileURL
|
|
122
|
+
let canonicalTargetPath = canonicalTarget.path
|
|
123
|
+
|
|
124
|
+
if canonicalTargetPath != canonicalBasePath && !canonicalTargetPath.hasPrefix(normalizedBasePath) {
|
|
125
|
+
throw SecurePathError.pathTraversal
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return canonicalTarget
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static func resolveManifestTargetPath(baseDirectory: URL, fileName: String) throws -> URL {
|
|
132
|
+
let isBrotli = fileName.hasSuffix(".br")
|
|
133
|
+
let targetFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
134
|
+
return try resolvePathInsideDirectory(baseDirectory: baseDirectory, relativePath: targetFileName)
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
private func isTimedOutError(_ error: Error?) -> Bool {
|
|
101
138
|
guard let nsError = error as NSError? else {
|
|
102
139
|
return false
|
|
@@ -491,21 +528,15 @@ import UIKit
|
|
|
491
528
|
}
|
|
492
529
|
}
|
|
493
530
|
|
|
494
|
-
private func
|
|
495
|
-
|
|
496
|
-
|
|
531
|
+
private func resolveZipEntry(path: String, destUnZip: URL) throws -> URL {
|
|
532
|
+
do {
|
|
533
|
+
return try Self.resolvePathInsideDirectory(baseDirectory: destUnZip, relativePath: path)
|
|
534
|
+
} catch SecurePathError.windowsPath {
|
|
497
535
|
logger.error("Unzip failed: Windows path not supported")
|
|
498
536
|
logger.debug("Invalid path: \(path)")
|
|
499
537
|
self.sendStats(action: "windows_path_fail")
|
|
500
538
|
throw CustomError.cannotUnzip
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Check for path traversal
|
|
504
|
-
let fileURL = destUnZip.appendingPathComponent(path)
|
|
505
|
-
let canonicalPath = fileURL.standardizedFileURL.path
|
|
506
|
-
let canonicalDir = destUnZip.standardizedFileURL.path
|
|
507
|
-
|
|
508
|
-
if !canonicalPath.hasPrefix(canonicalDir) {
|
|
539
|
+
} catch {
|
|
509
540
|
self.sendStats(action: "canonical_path_fail")
|
|
510
541
|
throw CustomError.cannotUnzip
|
|
511
542
|
}
|
|
@@ -596,10 +627,7 @@ import UIKit
|
|
|
596
627
|
|
|
597
628
|
do {
|
|
598
629
|
for entry in archive {
|
|
599
|
-
|
|
600
|
-
try validateZipEntry(path: entry.path, destUnZip: destUnZip)
|
|
601
|
-
|
|
602
|
-
let destPath = destUnZip.appendingPathComponent(entry.path)
|
|
630
|
+
let destPath = try resolveZipEntry(path: entry.path, destUnZip: destUnZip)
|
|
603
631
|
|
|
604
632
|
if entry.type == .directory {
|
|
605
633
|
try FileManager.default.createDirectory(at: destPath, withIntermediateDirectories: true, attributes: nil)
|
|
@@ -1100,8 +1128,22 @@ import UIKit
|
|
|
1100
1128
|
let legacyCacheFilePath: URL? = isBrotli ? cacheFolder.appendingPathComponent("\(finalFileHash)_\(fileNameWithoutPath)") : nil
|
|
1101
1129
|
|
|
1102
1130
|
let destFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
|
|
1103
|
-
let destFilePath
|
|
1104
|
-
let builtinFilePath
|
|
1131
|
+
let destFilePath: URL
|
|
1132
|
+
let builtinFilePath: URL
|
|
1133
|
+
do {
|
|
1134
|
+
destFilePath = try Self.resolveManifestTargetPath(baseDirectory: destFolder, fileName: fileName)
|
|
1135
|
+
builtinFilePath = try Self.resolvePathInsideDirectory(baseDirectory: builtinFolder, relativePath: fileName)
|
|
1136
|
+
} catch {
|
|
1137
|
+
logger.error("Invalid manifest file path: \(fileName)")
|
|
1138
|
+
self.sendStats(action: "manifest_path_fail", versionName: "\(version):\(fileName)")
|
|
1139
|
+
errorLock.lock()
|
|
1140
|
+
if downloadError == nil {
|
|
1141
|
+
downloadError = error
|
|
1142
|
+
}
|
|
1143
|
+
errorLock.unlock()
|
|
1144
|
+
hasError.value = true
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1105
1147
|
|
|
1106
1148
|
// Create parent directories synchronously (before operations start)
|
|
1107
1149
|
try? FileManager.default.createDirectory(at: destFilePath.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
|