@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.
@@ -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.2";
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
- if (entry.getName().contains("\\")) {
217
- logger.error("Unzip failed: Windows path not supported");
218
- logger.debug("Invalid path: " + entry.getName());
219
- this.sendStats("windows_path_fail");
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 = new File(destFolder, targetFileName);
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.2"
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 validateZipEntry(path: String, destUnZip: URL) throws {
495
- // Check for Windows paths
496
- if path.contains("\\") {
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
- // Validate entry path for security
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 = destFolder.appendingPathComponent(destFileName)
1104
- let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.47.2",
3
+ "version": "8.47.3",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",