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 +72 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +71 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
254
|
-
const isCaseInsensitiveFS = process.platform === "win32"
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|