@hot-updater/react-native 0.36.3 → 0.36.4

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.
@@ -13,7 +13,7 @@ class TarArchiveInputStream(
13
13
  ) : InputStream() {
14
14
  private var currentEntry: TarArchiveEntry? = null
15
15
  private var currentEntryBytesRead: Long = 0
16
- private var longName: String? = null
16
+ private var pendingName: String? = null
17
17
 
18
18
  companion object {
19
19
  private const val TAG = "TarInputStream"
@@ -73,14 +73,20 @@ class TarArchiveInputStream(
73
73
 
74
74
  // Handle GNU long filename extension
75
75
  if (entry.typeFlag == 'L') {
76
- longName = readLongName(entry.size)
76
+ pendingName = readLongName(entry.size)
77
77
  continue
78
78
  }
79
79
 
80
- // Apply long name if present
81
- if (longName != null) {
82
- entry.name = longName!!
83
- longName = null
80
+ // Handle POSIX PAX extended header
81
+ if (entry.typeFlag == 'x') {
82
+ readPaxPath(entry.size)?.let { pendingName = it }
83
+ continue
84
+ }
85
+
86
+ // Apply extended name if present
87
+ if (pendingName != null) {
88
+ entry.name = pendingName!!
89
+ pendingName = null
84
90
  }
85
91
 
86
92
  // Validate entry
@@ -306,6 +312,95 @@ class TarArchiveInputStream(
306
312
  return String(nameBytes, 0, nameLength, Charsets.UTF_8)
307
313
  }
308
314
 
315
+ /**
316
+ * Read the path attribute from a POSIX PAX extended header.
317
+ * Record lengths are measured in bytes and include the length itself.
318
+ */
319
+ private fun readPaxPath(size: Long): String? {
320
+ if (size < 0 || size > MAX_FILE_SIZE) {
321
+ throw SecurityException("Invalid PAX header size: $size")
322
+ }
323
+
324
+ val data = ByteArray(size.toInt())
325
+ var bytesRead = 0
326
+ while (bytesRead < data.size) {
327
+ val count = input.read(data, bytesRead, data.size - bytesRead)
328
+ if (count < 0) throw EOFException("Unexpected end reading PAX header")
329
+ bytesRead += count
330
+ }
331
+ skipPadding(size)
332
+
333
+ var offset = 0
334
+ var path: String? = null
335
+ while (offset < data.size) {
336
+ val space = findByte(data, ' '.code.toByte(), offset, data.size)
337
+ if (space <= offset) throw IOException("Invalid PAX record length")
338
+
339
+ val recordLength = parsePaxRecordLength(data, offset, space)
340
+ if (recordLength <= 0 || recordLength > data.size - offset) {
341
+ throw IOException("Invalid PAX record length: $recordLength")
342
+ }
343
+
344
+ val recordEnd = offset + recordLength
345
+ if (recordEnd <= space + 2 || data[recordEnd - 1] != '\n'.code.toByte()) {
346
+ throw IOException("Invalid PAX record")
347
+ }
348
+
349
+ val valueEnd = recordEnd - 1
350
+ val equals = findByte(data, '='.code.toByte(), space + 1, valueEnd)
351
+ if (equals <= space + 1) throw IOException("Invalid PAX record")
352
+
353
+ val key =
354
+ String(
355
+ data,
356
+ space + 1,
357
+ equals - space - 1,
358
+ Charsets.UTF_8,
359
+ )
360
+ if (key == "path") {
361
+ path =
362
+ String(
363
+ data,
364
+ equals + 1,
365
+ valueEnd - equals - 1,
366
+ Charsets.UTF_8,
367
+ )
368
+ }
369
+
370
+ offset = recordEnd
371
+ }
372
+
373
+ return path
374
+ }
375
+
376
+ private fun parsePaxRecordLength(
377
+ bytes: ByteArray,
378
+ start: Int,
379
+ end: Int,
380
+ ): Int {
381
+ var result = 0
382
+ for (index in start until end) {
383
+ val digit = bytes[index].toInt() - '0'.code
384
+ if (digit !in 0..9 || result > (Int.MAX_VALUE - digit) / 10) {
385
+ throw IOException("Invalid PAX record length")
386
+ }
387
+ result = result * 10 + digit
388
+ }
389
+ return result
390
+ }
391
+
392
+ private fun findByte(
393
+ bytes: ByteArray,
394
+ value: Byte,
395
+ start: Int,
396
+ end: Int,
397
+ ): Int {
398
+ for (index in start until end) {
399
+ if (bytes[index] == value) return index
400
+ }
401
+ return -1
402
+ }
403
+
309
404
  /**
310
405
  * Skip padding to 512-byte boundary
311
406
  */
@@ -0,0 +1,68 @@
1
+ package com.hotupdater
2
+
3
+ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
4
+ import org.junit.Assert.assertArrayEquals
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertNull
7
+ import org.junit.Assert.assertThrows
8
+ import org.junit.Test
9
+ import java.io.ByteArrayInputStream
10
+ import java.io.ByteArrayOutputStream
11
+ import org.apache.commons.compress.archivers.tar.TarArchiveEntry as CommonsTarArchiveEntry
12
+
13
+ class TarArchiveInputStreamTest {
14
+ @Test
15
+ fun `reads a long basename from a POSIX PAX header`() {
16
+ val path = "raw/${"long-name-".repeat(14)}asset.bmp"
17
+ val contents = "pax asset".toByteArray()
18
+ val archive = createPaxArchive(path, contents)
19
+
20
+ TarArchiveInputStream(ByteArrayInputStream(archive)).use { tar ->
21
+ val entry = tar.getNextEntry()
22
+
23
+ assertEquals(path, entry?.name)
24
+ assertArrayEquals(contents, tar.readBytes())
25
+ assertNull(tar.getNextEntry())
26
+ }
27
+ }
28
+
29
+ @Test
30
+ fun `rejects path traversal from a POSIX PAX header`() {
31
+ val path = "../${"long-name-".repeat(14)}asset.bmp"
32
+ val archive =
33
+ createPaxArchive(
34
+ path = path,
35
+ contents = "malicious".toByteArray(),
36
+ headerPath = "safe.bmp",
37
+ )
38
+
39
+ val error =
40
+ assertThrows(SecurityException::class.java) {
41
+ TarArchiveInputStream(ByteArrayInputStream(archive)).use { tar ->
42
+ tar.getNextEntry()
43
+ }
44
+ }
45
+
46
+ assertEquals("Path traversal detected: $path", error.message)
47
+ }
48
+
49
+ private fun createPaxArchive(
50
+ path: String,
51
+ contents: ByteArray,
52
+ headerPath: String = path,
53
+ ): ByteArray {
54
+ val output = ByteArrayOutputStream()
55
+ TarArchiveOutputStream(output).use { tar ->
56
+ tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX)
57
+ val entry =
58
+ CommonsTarArchiveEntry(headerPath).apply {
59
+ size = contents.size.toLong()
60
+ if (path != headerPath) addPaxHeader("path", path)
61
+ }
62
+ tar.putArchiveEntry(entry)
63
+ tar.write(contents)
64
+ tar.closeArchiveEntry()
65
+ }
66
+ return output.toByteArray()
67
+ }
68
+ }
@@ -4,5 +4,5 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.HOT_UPDATER_SDK_VERSION = void 0;
7
- const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.3";
7
+ const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.4";
8
8
  //# sourceMappingURL=sdkVersion.js.map
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
 
3
- export const HOT_UPDATER_SDK_VERSION = "0.36.3";
3
+ export const HOT_UPDATER_SDK_VERSION = "0.36.4";
4
4
  //# sourceMappingURL=sdkVersion.js.map
@@ -1,2 +1,2 @@
1
- export declare const HOT_UPDATER_SDK_VERSION = "0.36.3";
1
+ export declare const HOT_UPDATER_SDK_VERSION = "0.36.4";
2
2
  //# sourceMappingURL=sdkVersion.d.ts.map
@@ -1,2 +1,2 @@
1
- export declare const HOT_UPDATER_SDK_VERSION = "0.36.3";
1
+ export declare const HOT_UPDATER_SDK_VERSION = "0.36.4";
2
2
  //# sourceMappingURL=sdkVersion.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.36.3",
3
+ "version": "0.36.4",
4
4
  "description": "React Native OTA solution for self-hosted",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/commonjs/index.js",
@@ -131,14 +131,14 @@
131
131
  "react-native-builder-bob": "0.41.0",
132
132
  "@typescript/native": "npm:typescript@^7.0.2",
133
133
  "typescript": "npm:@typescript/typescript6@^6.0.2",
134
- "hot-updater": "0.36.3"
134
+ "hot-updater": "0.36.4"
135
135
  },
136
136
  "dependencies": {
137
137
  "use-sync-external-store": "1.5.0",
138
- "@hot-updater/cli-tools": "0.36.3",
139
- "@hot-updater/js": "0.36.3",
140
- "@hot-updater/core": "0.36.3",
141
- "@hot-updater/plugin-core": "0.36.3"
138
+ "@hot-updater/cli-tools": "0.36.4",
139
+ "@hot-updater/core": "0.36.4",
140
+ "@hot-updater/js": "0.36.4",
141
+ "@hot-updater/plugin-core": "0.36.4"
142
142
  },
143
143
  "scripts": {
144
144
  "build": "node ./scripts/sync-sdk-version.mjs run -- bob build && tsc -p plugin/tsconfig.build.json",
package/src/sdkVersion.ts CHANGED
@@ -1 +1 @@
1
- export const HOT_UPDATER_SDK_VERSION = "0.36.3";
1
+ export const HOT_UPDATER_SDK_VERSION = "0.36.4";