qvdjs 0.9.1 → 0.9.2

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/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var fs = require('fs');
3
4
  var path = require('path');
4
- var fs2 = require('fs');
5
5
  var crypto = require('crypto');
6
6
  var xml2 = require('xml2js');
7
7
  var assert = require('assert');
@@ -10,8 +10,8 @@ var v8 = require('v8');
10
10
 
11
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
12
 
13
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
13
14
  var path__default = /*#__PURE__*/_interopDefault(path);
14
- var fs2__default = /*#__PURE__*/_interopDefault(fs2);
15
15
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
16
16
  var xml2__default = /*#__PURE__*/_interopDefault(xml2);
17
17
  var assert__default = /*#__PURE__*/_interopDefault(assert);
@@ -262,8 +262,8 @@ var init_QvdSymbol = __esm({
262
262
  };
263
263
  }
264
264
  });
265
- function isWithinDirectory(resolvedBaseDir, resolvedPath) {
266
- const isCaseInsensitiveFS = process.platform === "win32" || process.platform === "darwin";
265
+ function isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) {
266
+ const isCaseInsensitiveFS = process.platform === "win32";
267
267
  const base = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
268
268
  const target = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
269
269
  const relative = path__default.default.relative(base, target);
@@ -275,6 +275,55 @@ function isWithinDirectory(resolvedBaseDir, resolvedPath) {
275
275
  }
276
276
  return relative !== ".." && !relative.startsWith(`..${path__default.default.sep}`);
277
277
  }
278
+ function resolveDeepestExisting(target) {
279
+ let current = target;
280
+ for (; ; ) {
281
+ try {
282
+ return fs__default.default.realpathSync(current);
283
+ } catch (error) {
284
+ const code = (
285
+ /** @type {{code?: string}} */
286
+ error?.code
287
+ );
288
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
289
+ return null;
290
+ }
291
+ const parent = path__default.default.dirname(current);
292
+ if (parent === current) {
293
+ return null;
294
+ }
295
+ current = parent;
296
+ }
297
+ }
298
+ }
299
+ function isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath) {
300
+ let baseStat;
301
+ try {
302
+ baseStat = fs__default.default.statSync(fs__default.default.realpathSync(resolvedBaseDir));
303
+ } catch {
304
+ return null;
305
+ }
306
+ let current = resolveDeepestExisting(resolvedPath);
307
+ if (current === null) {
308
+ return null;
309
+ }
310
+ for (; ; ) {
311
+ let stat;
312
+ try {
313
+ stat = fs__default.default.statSync(current);
314
+ } catch {
315
+ return null;
316
+ }
317
+ if (stat.dev === baseStat.dev && stat.ino === baseStat.ino) {
318
+ return true;
319
+ }
320
+ const parent = path__default.default.dirname(current);
321
+ if (parent === current) {
322
+ return false;
323
+ }
324
+ current = parent;
325
+ }
326
+ }
278
327
  function validatePath(filePath, allowedDir) {
279
328
  if (typeof filePath !== "string" || filePath.length === 0) {
280
329
  throw new exports.QvdValidationError("filePath must be a non-empty string", {
@@ -297,12 +346,17 @@ function validatePath(filePath, allowedDir) {
297
346
  const resolvedPath = path__default.default.resolve(filePath);
298
347
  const baseDir = allowedDir || process.cwd();
299
348
  const resolvedBaseDir = path__default.default.resolve(baseDir);
300
- if (!isWithinDirectory(resolvedBaseDir, resolvedPath)) {
349
+ const onDisk = isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath);
350
+ const contained = onDisk === null ? isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) : onDisk;
351
+ if (!contained) {
301
352
  throw new exports.QvdSecurityError("Path traversal detected: Access denied", {
302
353
  path: filePath,
303
354
  resolvedPath,
304
355
  allowedDir: resolvedBaseDir,
305
- reason: "outside_allowed_directory"
356
+ reason: "outside_allowed_directory",
357
+ // Says which check refused, so a rejection of a path that looks contained is traceable to
358
+ // a symlink or a case difference rather than looking like a bug.
359
+ check: onDisk === null ? "lexical" : "filesystem"
306
360
  });
307
361
  }
308
362
  return resolvedPath;
@@ -331,7 +385,8 @@ var init_QvdFileWriter = __esm({
331
385
  * @param {QvdDataFrame} df The data frame to write to the QVD file.
332
386
  * @param {Object} [options={}] Options for the writer.
333
387
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
334
- * path must be within this directory. Defaults to the current working directory. To permit
388
+ * path must be within this directory, with symlinks resolved first, so a link inside it that
389
+ * points outside it is rejected. Defaults to the current working directory. To permit
335
390
  * an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
336
391
  * empty value falls back to the working directory rather than removing the restriction.
337
392
  * @param {Function} [options.onProgress] Optional progress callback function.
@@ -379,7 +434,7 @@ var init_QvdFileWriter = __esm({
379
434
  const headerBuffer = Buffer.concat([Buffer.from(this._header, "utf-8"), Buffer.from([0])]);
380
435
  let fd;
381
436
  try {
382
- fd = await fs2__default.default.promises.open(this._path, "w");
437
+ fd = await fs__default.default.promises.open(this._path, "w");
383
438
  await fd.write(headerBuffer, 0, headerBuffer.length, 0);
384
439
  await fd.write(this._symbolBuffer, 0, this._symbolBuffer.length, headerBuffer.length);
385
440
  await fd.write(this._indexBuffer, 0, this._indexBuffer.length, headerBuffer.length + this._symbolBuffer.length);
@@ -1242,7 +1297,8 @@ var init_QvdFileReader = __esm({
1242
1297
  * @param {string} filePath The path to the QVD file to load.
1243
1298
  * @param {Object} [options={}] Options for the reader.
1244
1299
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
1245
- * path must be within this directory. Defaults to the current working directory. To permit
1300
+ * path must be within this directory, with symlinks resolved first, so a link inside it that
1301
+ * points outside it is rejected. Defaults to the current working directory. To permit
1246
1302
  * an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
1247
1303
  * empty value falls back to the working directory rather than removing the restriction.
1248
1304
  * @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
@@ -1291,13 +1347,13 @@ var init_QvdFileReader = __esm({
1291
1347
  */
1292
1348
  async _readData(maxRows = null) {
1293
1349
  if (maxRows === null) {
1294
- this._buffer = await fs2__default.default.promises.readFile(this._path);
1350
+ this._buffer = await fs__default.default.promises.readFile(this._path);
1295
1351
  this._fileSize = this._buffer.length;
1296
1352
  return;
1297
1353
  }
1298
1354
  const HEADER_DELIMITER = "\r\n\0";
1299
1355
  const CHUNK_SIZE = 64 * 1024;
1300
- const stream = fs2__default.default.createReadStream(this._path, {
1356
+ const stream = fs__default.default.createReadStream(this._path, {
1301
1357
  highWaterMark: CHUNK_SIZE
1302
1358
  });
1303
1359
  const headerChunks = [];
@@ -1380,7 +1436,7 @@ var init_QvdFileReader = __esm({
1380
1436
  }
1381
1437
  const indexTableBytesToRead = rowsToLoad * recordSize;
1382
1438
  const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
1383
- const fd = await fs2__default.default.promises.open(this._path, "r");
1439
+ const fd = await fs__default.default.promises.open(this._path, "r");
1384
1440
  try {
1385
1441
  const { size: fileSize } = await fd.stat();
1386
1442
  this._fileSize = fileSize;
@@ -2126,7 +2182,8 @@ var init_QvdDataFrame = __esm({
2126
2182
  * @param {string} path The path to the QVD file.
2127
2183
  * @param {Object} [options] Optional writing options.
2128
2184
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
2129
- * must be within this directory. Defaults to the current working directory. To permit an entire
2185
+ * must be within this directory, with symlinks resolved first, so a link inside it that points
2186
+ * outside it is rejected. Defaults to the current working directory. To permit an entire
2130
2187
  * volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
2131
2188
  * back to the working directory rather than removing the restriction.
2132
2189
  * @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
@@ -2147,7 +2204,8 @@ var init_QvdDataFrame = __esm({
2147
2204
  * @param {number|null} [options.maxRows] The maximum number of rows to load. Must be a non-negative
2148
2205
  * integer; if not specified or null, all rows are loaded. Anything else throws a QvdValidationError.
2149
2206
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
2150
- * must be within this directory. Defaults to the current working directory. To permit an entire
2207
+ * must be within this directory, with symlinks resolved first, so a link inside it that points
2208
+ * outside it is rejected. Defaults to the current working directory. To permit an entire
2151
2209
  * volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
2152
2210
  * back to the working directory rather than removing the restriction.
2153
2211
  * @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage