@softtynet/zca-js 2.3.5 → 2.3.6

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.
@@ -10,13 +10,31 @@ export const downloadTransferSyncFactory = apiFactory()((api, ctx, utils) => {
10
10
  return async function downloadTransferSync(options) {
11
11
  let downloadUrl = "";
12
12
  if (typeof options === "string") {
13
+ let parsedUrl;
14
+ try {
15
+ parsedUrl = new URL(options);
16
+ }
17
+ catch {
18
+ throw new ZaloApiError("Invalid downloadTransferSync URL");
19
+ }
20
+ if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
21
+ throw new ZaloApiError(`Unsupported protocol in downloadTransferSync URL: ${parsedUrl.protocol}`);
22
+ }
23
+ if (parsedUrl.hostname === "169.254.169.254" || parsedUrl.hostname === "metadata.google.internal") {
24
+ throw new ZaloApiError("Blocked internal metadata hostname in downloadTransferSync URL");
25
+ }
13
26
  downloadUrl = options;
14
27
  }
15
28
  else {
16
29
  if (!options.key || !options.exp || !options.sig) {
17
30
  throw new ZaloApiError("Missing key, exp or sig in options");
18
31
  }
19
- downloadUrl = `${ZALO_DOMAINS.TRANSFER_SYNC}${ZALO_ENDPOINTS.TRANSFER_SYNC.DOWNLOAD_V2}?key=${options.key}&exp=${options.exp}&sig=${options.sig}`;
32
+ const qs = new URLSearchParams({
33
+ key: String(options.key),
34
+ exp: String(options.exp),
35
+ sig: String(options.sig),
36
+ });
37
+ downloadUrl = `${ZALO_DOMAINS.TRANSFER_SYNC}${ZALO_ENDPOINTS.TRANSFER_SYNC.DOWNLOAD_V2}?${qs.toString()}`;
20
38
  }
21
39
  const response = await utils.request(downloadUrl, {
22
40
  method: "GET",
@@ -8,6 +8,7 @@ import { Buffer } from "node:buffer";
8
8
  */
9
9
  export const DEFAULT_ZALOCALL_IPC_KEY_BASE64 = "yjAF9oqMWl6XfXYJn9mA7w==";
10
10
  const MAX_IPC_CHUNK_LENGTH = 4000;
11
+ const MAX_IPC_RECEIVE_BUFFER_LENGTH = 4 * 1024 * 1024;
11
12
  /**
12
13
  * Tầng giao thức IPC双kênh (`receiveSocketPath` & `sendSocketPath`) mã hoá AES-128-CBC (Hex + `$` framing + 4000-char chunking)
13
14
  * khớp 100% với module `3Zc2` / `4KIH` của Zalo PC và `zrtc_call::ZIpcClientMac` (`0x10012eb50`).
@@ -107,11 +108,31 @@ export class ZaloCallTransport extends EventEmitter {
107
108
  await Promise.all([
108
109
  new Promise((resolve, reject) => {
109
110
  this.receiveServer.once("error", reject);
110
- this.receiveServer.listen(this.receiveSocketPath, () => resolve());
111
+ this.receiveServer.listen(this.receiveSocketPath, () => {
112
+ if (process.platform !== "win32") {
113
+ try {
114
+ fs.chmodSync(this.receiveSocketPath, 0o600);
115
+ }
116
+ catch {
117
+ // Ignore chmod error if unsupported
118
+ }
119
+ }
120
+ resolve();
121
+ });
111
122
  }),
112
123
  new Promise((resolve, reject) => {
113
124
  this.sendServer.once("error", reject);
114
- this.sendServer.listen(this.sendSocketPath, () => resolve());
125
+ this.sendServer.listen(this.sendSocketPath, () => {
126
+ if (process.platform !== "win32") {
127
+ try {
128
+ fs.chmodSync(this.sendSocketPath, 0o600);
129
+ }
130
+ catch {
131
+ // Ignore chmod error if unsupported
132
+ }
133
+ }
134
+ resolve();
135
+ });
115
136
  }),
116
137
  ]);
117
138
  this.emit("ready");
@@ -200,6 +221,11 @@ export class ZaloCallTransport extends EventEmitter {
200
221
  }
201
222
  }
202
223
  handleReceiveChunk(chunk) {
224
+ if (this.receiveBuffer.length + chunk.length > MAX_IPC_RECEIVE_BUFFER_LENGTH) {
225
+ this.receiveBuffer = "";
226
+ this.emit("error", new Error("IPC receive buffer exceeded maximum allowed size (4MB)"));
227
+ return;
228
+ }
203
229
  this.receiveBuffer += chunk;
204
230
  let dollarIdx = this.receiveBuffer.indexOf("$");
205
231
  while (dollarIdx !== -1) {
@@ -2789,7 +2789,12 @@ export class ZaloRtcSession extends EventEmitter {
2789
2789
  }
2790
2790
  }
2791
2791
  else if (this.fuaFragments.length > 0) {
2792
- this.fuaFragments.push(body);
2792
+ if (this.fuaFragments.length >= 512) {
2793
+ this.fuaFragments = [];
2794
+ }
2795
+ else {
2796
+ this.fuaFragments.push(body);
2797
+ }
2793
2798
  }
2794
2799
  if (isEnd && this.fuaFragments.length > 0) {
2795
2800
  const fullNal = Buffer.concat(this.fuaFragments);
@@ -2823,6 +2828,9 @@ export class ZaloRtcSession extends EventEmitter {
2823
2828
  else if (nalType === 5) {
2824
2829
  this.currentAccessUnitHasIdr = true;
2825
2830
  }
2831
+ if (this.currentAccessUnitNals.length >= 256) {
2832
+ this.currentAccessUnitNals = [];
2833
+ }
2826
2834
  this.currentAccessUnitNals.push(Buffer.from(nal));
2827
2835
  }
2828
2836
  flushIncomingAccessUnit(timestamp, ssrc) {
@@ -15,13 +15,31 @@ const downloadTransferSyncFactory = utils.apiFactory()((api, ctx, utils) => {
15
15
  return async function downloadTransferSync(options) {
16
16
  let downloadUrl = "";
17
17
  if (typeof options === "string") {
18
+ let parsedUrl;
19
+ try {
20
+ parsedUrl = new URL(options);
21
+ }
22
+ catch {
23
+ throw new ZaloApiError.ZaloApiError("Invalid downloadTransferSync URL");
24
+ }
25
+ if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
26
+ throw new ZaloApiError.ZaloApiError(`Unsupported protocol in downloadTransferSync URL: ${parsedUrl.protocol}`);
27
+ }
28
+ if (parsedUrl.hostname === "169.254.169.254" || parsedUrl.hostname === "metadata.google.internal") {
29
+ throw new ZaloApiError.ZaloApiError("Blocked internal metadata hostname in downloadTransferSync URL");
30
+ }
18
31
  downloadUrl = options;
19
32
  }
20
33
  else {
21
34
  if (!options.key || !options.exp || !options.sig) {
22
35
  throw new ZaloApiError.ZaloApiError("Missing key, exp or sig in options");
23
36
  }
24
- downloadUrl = `${domains.ZALO_DOMAINS.TRANSFER_SYNC}${endpoints.ZALO_ENDPOINTS.TRANSFER_SYNC.DOWNLOAD_V2}?key=${options.key}&exp=${options.exp}&sig=${options.sig}`;
37
+ const qs = new URLSearchParams({
38
+ key: String(options.key),
39
+ exp: String(options.exp),
40
+ sig: String(options.sig),
41
+ });
42
+ downloadUrl = `${domains.ZALO_DOMAINS.TRANSFER_SYNC}${endpoints.ZALO_ENDPOINTS.TRANSFER_SYNC.DOWNLOAD_V2}?${qs.toString()}`;
25
43
  }
26
44
  const response = await utils.request(downloadUrl, {
27
45
  method: "GET",
@@ -11,6 +11,7 @@ var node_buffer = require('node:buffer');
11
11
  */
12
12
  const DEFAULT_ZALOCALL_IPC_KEY_BASE64 = "yjAF9oqMWl6XfXYJn9mA7w==";
13
13
  const MAX_IPC_CHUNK_LENGTH = 4000;
14
+ const MAX_IPC_RECEIVE_BUFFER_LENGTH = 4 * 1024 * 1024;
14
15
  /**
15
16
  * Tầng giao thức IPC双kênh (`receiveSocketPath` & `sendSocketPath`) mã hoá AES-128-CBC (Hex + `$` framing + 4000-char chunking)
16
17
  * khớp 100% với module `3Zc2` / `4KIH` của Zalo PC và `zrtc_call::ZIpcClientMac` (`0x10012eb50`).
@@ -110,11 +111,31 @@ class ZaloCallTransport extends EventEmitter {
110
111
  await Promise.all([
111
112
  new Promise((resolve, reject) => {
112
113
  this.receiveServer.once("error", reject);
113
- this.receiveServer.listen(this.receiveSocketPath, () => resolve());
114
+ this.receiveServer.listen(this.receiveSocketPath, () => {
115
+ if (process.platform !== "win32") {
116
+ try {
117
+ fs.chmodSync(this.receiveSocketPath, 0o600);
118
+ }
119
+ catch {
120
+ // Ignore chmod error if unsupported
121
+ }
122
+ }
123
+ resolve();
124
+ });
114
125
  }),
115
126
  new Promise((resolve, reject) => {
116
127
  this.sendServer.once("error", reject);
117
- this.sendServer.listen(this.sendSocketPath, () => resolve());
128
+ this.sendServer.listen(this.sendSocketPath, () => {
129
+ if (process.platform !== "win32") {
130
+ try {
131
+ fs.chmodSync(this.sendSocketPath, 0o600);
132
+ }
133
+ catch {
134
+ // Ignore chmod error if unsupported
135
+ }
136
+ }
137
+ resolve();
138
+ });
118
139
  }),
119
140
  ]);
120
141
  this.emit("ready");
@@ -203,6 +224,11 @@ class ZaloCallTransport extends EventEmitter {
203
224
  }
204
225
  }
205
226
  handleReceiveChunk(chunk) {
227
+ if (this.receiveBuffer.length + chunk.length > MAX_IPC_RECEIVE_BUFFER_LENGTH) {
228
+ this.receiveBuffer = "";
229
+ this.emit("error", new Error("IPC receive buffer exceeded maximum allowed size (4MB)"));
230
+ return;
231
+ }
206
232
  this.receiveBuffer += chunk;
207
233
  let dollarIdx = this.receiveBuffer.indexOf("$");
208
234
  while (dollarIdx !== -1) {
@@ -2792,7 +2792,12 @@ class ZaloRtcSession extends EventEmitter {
2792
2792
  }
2793
2793
  }
2794
2794
  else if (this.fuaFragments.length > 0) {
2795
- this.fuaFragments.push(body);
2795
+ if (this.fuaFragments.length >= 512) {
2796
+ this.fuaFragments = [];
2797
+ }
2798
+ else {
2799
+ this.fuaFragments.push(body);
2800
+ }
2796
2801
  }
2797
2802
  if (isEnd && this.fuaFragments.length > 0) {
2798
2803
  const fullNal = node_buffer.Buffer.concat(this.fuaFragments);
@@ -2826,6 +2831,9 @@ class ZaloRtcSession extends EventEmitter {
2826
2831
  else if (nalType === 5) {
2827
2832
  this.currentAccessUnitHasIdr = true;
2828
2833
  }
2834
+ if (this.currentAccessUnitNals.length >= 256) {
2835
+ this.currentAccessUnitNals = [];
2836
+ }
2829
2837
  this.currentAccessUnitNals.push(node_buffer.Buffer.from(nal));
2830
2838
  }
2831
2839
  flushIncomingAccessUnit(timestamp, ssrc) {
@@ -12,7 +12,7 @@ const ZALO_PROTOCOL = {
12
12
  API_VERSION: 689,
13
13
  DEFAULT_API_VERSION: 689,
14
14
  /** Phiên bản SDK hiện tại */
15
- SDK_VERSION: "2.3.5",
15
+ SDK_VERSION: "2.3.6",
16
16
  /** Mã zpw_type đặc thù dành riêng cho phân hệ Gọi Thoại & Video (binary C++ ZaloCall) */
17
17
  VOICECALL_API_TYPE: 24,
18
18
  /** Phiên bản mã hóa ZCID (ParamDecryptor) */
@@ -170,7 +170,9 @@ function encryptPin(pin) {
170
170
  }
171
171
  function validatePin(encryptedPin, pin) {
172
172
  const hash = crypto.createHash("md5").update(pin).digest("hex");
173
- return hash === encryptedPin;
173
+ const a = Buffer.from(hash, "utf8");
174
+ const b = Buffer.from(String(encryptedPin), "utf8");
175
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
174
176
  }
175
177
 
176
178
  exports.ParamsEncryptor = ParamsEncryptor;
@@ -149,6 +149,9 @@ function parseZdbHeader(zdbPlaintext) {
149
149
  throw new ZaloApiError.ZaloApiError("Buffer is not a valid decrypted ZDB4.0 container");
150
150
  }
151
151
  const count = zdbPlaintext.readUInt32BE(COUNT_OFFSET);
152
+ if (count > 100000) {
153
+ throw new ZaloApiError.ZaloApiError(`ZDB4.0 header entry count exceeds safety limit (${count})`);
154
+ }
152
155
  let offset = COUNT_OFFSET + 4;
153
156
  const entries = [];
154
157
  for (let i = 0; i < count; i++) {
@@ -156,12 +159,16 @@ function parseZdbHeader(zdbPlaintext) {
156
159
  throw new ZaloApiError.ZaloApiError("Truncated ZDB4.0 header while reading entry name length");
157
160
  }
158
161
  const nameLen = zdbPlaintext.readUInt32BE(offset);
159
- if (offset + 4 + nameLen + 4 > zdbPlaintext.length) {
160
- throw new ZaloApiError.ZaloApiError("Truncated ZDB4.0 header while reading entry metadata");
162
+ if (nameLen > 4096 || offset + 4 + nameLen + 4 > zdbPlaintext.length) {
163
+ throw new ZaloApiError.ZaloApiError("Truncated or invalid ZDB4.0 header while reading entry metadata");
161
164
  }
162
- const name = zdbPlaintext
165
+ const rawName = zdbPlaintext
163
166
  .toString("utf8", offset + 4, offset + 4 + nameLen)
164
167
  .replace(/\0+$/, "");
168
+ const name = rawName.split(/[\\/]/).pop() || rawName;
169
+ if (name === ".." || name === "." || name.includes("..")) {
170
+ throw new ZaloApiError.ZaloApiError(`Unsafe entry name in ZDB4.0 header: ${rawName}`);
171
+ }
165
172
  const size = zdbPlaintext.readUInt32BE(offset + 4 + nameLen);
166
173
  entries.push({ name, size });
167
174
  offset += 4 + nameLen + 4;
@@ -10,7 +10,7 @@ export declare const ZALO_PROTOCOL: {
10
10
  readonly API_VERSION: 689;
11
11
  readonly DEFAULT_API_VERSION: 689;
12
12
  /** Phiên bản SDK hiện tại */
13
- readonly SDK_VERSION: "2.3.5";
13
+ readonly SDK_VERSION: "2.3.6";
14
14
  /** Mã zpw_type đặc thù dành riêng cho phân hệ Gọi Thoại & Video (binary C++ ZaloCall) */
15
15
  readonly VOICECALL_API_TYPE: 24;
16
16
  /** Phiên bản mã hóa ZCID (ParamDecryptor) */
@@ -10,7 +10,7 @@ export const ZALO_PROTOCOL = {
10
10
  API_VERSION: 689,
11
11
  DEFAULT_API_VERSION: 689,
12
12
  /** Phiên bản SDK hiện tại */
13
- SDK_VERSION: "2.3.5",
13
+ SDK_VERSION: "2.3.6",
14
14
  /** Mã zpw_type đặc thù dành riêng cho phân hệ Gọi Thoại & Video (binary C++ ZaloCall) */
15
15
  VOICECALL_API_TYPE: 24,
16
16
  /** Phiên bản mã hóa ZCID (ParamDecryptor) */
@@ -167,5 +167,7 @@ export function encryptPin(pin) {
167
167
  }
168
168
  export function validatePin(encryptedPin, pin) {
169
169
  const hash = crypto.createHash("md5").update(pin).digest("hex");
170
- return hash === encryptedPin;
170
+ const a = Buffer.from(hash, "utf8");
171
+ const b = Buffer.from(String(encryptedPin), "utf8");
172
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
171
173
  }
@@ -146,6 +146,9 @@ export function parseZdbHeader(zdbPlaintext) {
146
146
  throw new ZaloApiError("Buffer is not a valid decrypted ZDB4.0 container");
147
147
  }
148
148
  const count = zdbPlaintext.readUInt32BE(COUNT_OFFSET);
149
+ if (count > 100000) {
150
+ throw new ZaloApiError(`ZDB4.0 header entry count exceeds safety limit (${count})`);
151
+ }
149
152
  let offset = COUNT_OFFSET + 4;
150
153
  const entries = [];
151
154
  for (let i = 0; i < count; i++) {
@@ -153,12 +156,16 @@ export function parseZdbHeader(zdbPlaintext) {
153
156
  throw new ZaloApiError("Truncated ZDB4.0 header while reading entry name length");
154
157
  }
155
158
  const nameLen = zdbPlaintext.readUInt32BE(offset);
156
- if (offset + 4 + nameLen + 4 > zdbPlaintext.length) {
157
- throw new ZaloApiError("Truncated ZDB4.0 header while reading entry metadata");
159
+ if (nameLen > 4096 || offset + 4 + nameLen + 4 > zdbPlaintext.length) {
160
+ throw new ZaloApiError("Truncated or invalid ZDB4.0 header while reading entry metadata");
158
161
  }
159
- const name = zdbPlaintext
162
+ const rawName = zdbPlaintext
160
163
  .toString("utf8", offset + 4, offset + 4 + nameLen)
161
164
  .replace(/\0+$/, "");
165
+ const name = rawName.split(/[\\/]/).pop() || rawName;
166
+ if (name === ".." || name === "." || name.includes("..")) {
167
+ throw new ZaloApiError(`Unsafe entry name in ZDB4.0 header: ${rawName}`);
168
+ }
162
169
  const size = zdbPlaintext.readUInt32BE(offset + 4 + nameLen);
163
170
  entries.push({ name, size });
164
171
  offset += 4 + nameLen + 4;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softtynet/zca-js",
3
- "version": "2.3.5",
3
+ "version": "2.3.6",
4
4
  "description": "Zalo API client library for Node.js and Bun, maintained by Softtynet.",
5
5
  "main": "dist/cjs/index.cjs",
6
6
  "module": "dist/index.js",