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.js CHANGED
@@ -1,5 +1,5 @@
1
+ import fs from 'fs';
1
2
  import path from 'path';
2
- import fs2 from 'fs';
3
3
  import crypto from 'crypto';
4
4
  import xml2 from 'xml2js';
5
5
  import assert from 'assert';
@@ -250,8 +250,8 @@ var init_QvdSymbol = __esm({
250
250
  };
251
251
  }
252
252
  });
253
- function isWithinDirectory(resolvedBaseDir, resolvedPath) {
254
- const isCaseInsensitiveFS = process.platform === "win32" || process.platform === "darwin";
253
+ function isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) {
254
+ const isCaseInsensitiveFS = process.platform === "win32";
255
255
  const base = isCaseInsensitiveFS ? resolvedBaseDir.toLowerCase() : resolvedBaseDir;
256
256
  const target = isCaseInsensitiveFS ? resolvedPath.toLowerCase() : resolvedPath;
257
257
  const relative = path.relative(base, target);
@@ -263,6 +263,55 @@ function isWithinDirectory(resolvedBaseDir, resolvedPath) {
263
263
  }
264
264
  return relative !== ".." && !relative.startsWith(`..${path.sep}`);
265
265
  }
266
+ function resolveDeepestExisting(target) {
267
+ let current = target;
268
+ for (; ; ) {
269
+ try {
270
+ return fs.realpathSync(current);
271
+ } catch (error) {
272
+ const code = (
273
+ /** @type {{code?: string}} */
274
+ error?.code
275
+ );
276
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
277
+ return null;
278
+ }
279
+ const parent = path.dirname(current);
280
+ if (parent === current) {
281
+ return null;
282
+ }
283
+ current = parent;
284
+ }
285
+ }
286
+ }
287
+ function isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath) {
288
+ let baseStat;
289
+ try {
290
+ baseStat = fs.statSync(fs.realpathSync(resolvedBaseDir));
291
+ } catch {
292
+ return null;
293
+ }
294
+ let current = resolveDeepestExisting(resolvedPath);
295
+ if (current === null) {
296
+ return null;
297
+ }
298
+ for (; ; ) {
299
+ let stat;
300
+ try {
301
+ stat = fs.statSync(current);
302
+ } catch {
303
+ return null;
304
+ }
305
+ if (stat.dev === baseStat.dev && stat.ino === baseStat.ino) {
306
+ return true;
307
+ }
308
+ const parent = path.dirname(current);
309
+ if (parent === current) {
310
+ return false;
311
+ }
312
+ current = parent;
313
+ }
314
+ }
266
315
  function validatePath(filePath, allowedDir) {
267
316
  if (typeof filePath !== "string" || filePath.length === 0) {
268
317
  throw new QvdValidationError("filePath must be a non-empty string", {
@@ -285,12 +334,17 @@ function validatePath(filePath, allowedDir) {
285
334
  const resolvedPath = path.resolve(filePath);
286
335
  const baseDir = allowedDir || process.cwd();
287
336
  const resolvedBaseDir = path.resolve(baseDir);
288
- if (!isWithinDirectory(resolvedBaseDir, resolvedPath)) {
337
+ const onDisk = isWithinDirectoryOnDisk(resolvedBaseDir, resolvedPath);
338
+ const contained = onDisk === null ? isWithinDirectoryLexically(resolvedBaseDir, resolvedPath) : onDisk;
339
+ if (!contained) {
289
340
  throw new QvdSecurityError("Path traversal detected: Access denied", {
290
341
  path: filePath,
291
342
  resolvedPath,
292
343
  allowedDir: resolvedBaseDir,
293
- reason: "outside_allowed_directory"
344
+ reason: "outside_allowed_directory",
345
+ // Says which check refused, so a rejection of a path that looks contained is traceable to
346
+ // a symlink or a case difference rather than looking like a bug.
347
+ check: onDisk === null ? "lexical" : "filesystem"
294
348
  });
295
349
  }
296
350
  return resolvedPath;
@@ -319,7 +373,8 @@ var init_QvdFileWriter = __esm({
319
373
  * @param {QvdDataFrame} df The data frame to write to the QVD file.
320
374
  * @param {Object} [options={}] Options for the writer.
321
375
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
322
- * path must be within this directory. Defaults to the current working directory. To permit
376
+ * path must be within this directory, with symlinks resolved first, so a link inside it that
377
+ * points outside it is rejected. Defaults to the current working directory. To permit
323
378
  * an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
324
379
  * empty value falls back to the working directory rather than removing the restriction.
325
380
  * @param {Function} [options.onProgress] Optional progress callback function.
@@ -367,7 +422,7 @@ var init_QvdFileWriter = __esm({
367
422
  const headerBuffer = Buffer.concat([Buffer.from(this._header, "utf-8"), Buffer.from([0])]);
368
423
  let fd;
369
424
  try {
370
- fd = await fs2.promises.open(this._path, "w");
425
+ fd = await fs.promises.open(this._path, "w");
371
426
  await fd.write(headerBuffer, 0, headerBuffer.length, 0);
372
427
  await fd.write(this._symbolBuffer, 0, this._symbolBuffer.length, headerBuffer.length);
373
428
  await fd.write(this._indexBuffer, 0, this._indexBuffer.length, headerBuffer.length + this._symbolBuffer.length);
@@ -1230,7 +1285,8 @@ var init_QvdFileReader = __esm({
1230
1285
  * @param {string} filePath The path to the QVD file to load.
1231
1286
  * @param {Object} [options={}] Options for the reader.
1232
1287
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file
1233
- * path must be within this directory. Defaults to the current working directory. To permit
1288
+ * path must be within this directory, with symlinks resolved first, so a link inside it that
1289
+ * points outside it is rejected. Defaults to the current working directory. To permit
1234
1290
  * an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
1235
1291
  * empty value falls back to the working directory rather than removing the restriction.
1236
1292
  * @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
@@ -1279,13 +1335,13 @@ var init_QvdFileReader = __esm({
1279
1335
  */
1280
1336
  async _readData(maxRows = null) {
1281
1337
  if (maxRows === null) {
1282
- this._buffer = await fs2.promises.readFile(this._path);
1338
+ this._buffer = await fs.promises.readFile(this._path);
1283
1339
  this._fileSize = this._buffer.length;
1284
1340
  return;
1285
1341
  }
1286
1342
  const HEADER_DELIMITER = "\r\n\0";
1287
1343
  const CHUNK_SIZE = 64 * 1024;
1288
- const stream = fs2.createReadStream(this._path, {
1344
+ const stream = fs.createReadStream(this._path, {
1289
1345
  highWaterMark: CHUNK_SIZE
1290
1346
  });
1291
1347
  const headerChunks = [];
@@ -1368,7 +1424,7 @@ var init_QvdFileReader = __esm({
1368
1424
  }
1369
1425
  const indexTableBytesToRead = rowsToLoad * recordSize;
1370
1426
  const totalBytesToRead = indexTableOffset + indexTableBytesToRead;
1371
- const fd = await fs2.promises.open(this._path, "r");
1427
+ const fd = await fs.promises.open(this._path, "r");
1372
1428
  try {
1373
1429
  const { size: fileSize } = await fd.stat();
1374
1430
  this._fileSize = fileSize;
@@ -2114,7 +2170,8 @@ var init_QvdDataFrame = __esm({
2114
2170
  * @param {string} path The path to the QVD file.
2115
2171
  * @param {Object} [options] Optional writing options.
2116
2172
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
2117
- * must be within this directory. Defaults to the current working directory. To permit an entire
2173
+ * must be within this directory, with symlinks resolved first, so a link inside it that points
2174
+ * outside it is rejected. Defaults to the current working directory. To permit an entire
2118
2175
  * volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
2119
2176
  * back to the working directory rather than removing the restriction.
2120
2177
  * @param {Function} [options.onProgress] Optional progress callback function that receives progress updates during write operations.
@@ -2135,7 +2192,8 @@ var init_QvdDataFrame = __esm({
2135
2192
  * @param {number|null} [options.maxRows] The maximum number of rows to load. Must be a non-negative
2136
2193
  * integer; if not specified or null, all rows are loaded. Anything else throws a QvdValidationError.
2137
2194
  * @param {string} [options.allowedDir] Optional allowed directory path. If provided, the file path
2138
- * must be within this directory. Defaults to the current working directory. To permit an entire
2195
+ * must be within this directory, with symlinks resolved first, so a link inside it that points
2196
+ * outside it is rejected. Defaults to the current working directory. To permit an entire
2139
2197
  * volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
2140
2198
  * back to the working directory rather than removing the restriction.
2141
2199
  * @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage