@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.
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +101 -6
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +68 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/package.json +6 -6
- package/src/sdkVersion.ts +1 -1
|
@@ -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
|
|
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
|
-
|
|
76
|
+
pendingName = readLongName(entry.size)
|
|
77
77
|
continue
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
if (
|
|
82
|
-
entry.
|
|
83
|
-
|
|
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.
|
|
7
|
+
const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.4";
|
|
8
8
|
//# sourceMappingURL=sdkVersion.js.map
|
package/lib/module/sdkVersion.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HOT_UPDATER_SDK_VERSION = "0.36.
|
|
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.
|
|
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
|
+
"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.
|
|
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.
|
|
139
|
-
"@hot-updater/
|
|
140
|
-
"@hot-updater/
|
|
141
|
-
"@hot-updater/plugin-core": "0.36.
|
|
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.
|
|
1
|
+
export const HOT_UPDATER_SDK_VERSION = "0.36.4";
|