mirror of
https://github.com/docmost/docmost.git
synced 2026-07-27 11:44:50 +10:00
better zip handling
This commit is contained in:
@@ -122,7 +122,7 @@
|
|||||||
"typesense": "3.0.5",
|
"typesense": "3.0.5",
|
||||||
"undici": "7.28.0",
|
"undici": "7.28.0",
|
||||||
"ws": "8.21.0",
|
"ws": "8.21.0",
|
||||||
"yauzl": "3.2.1",
|
"yauzl": "3.4.0",
|
||||||
"zod": "4.3.6"
|
"zod": "4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -31,31 +31,39 @@ export function getFileTaskFolderPath(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const COMPRESSION_HEADROOM = 10;
|
||||||
* Extracts a ZIP archive.
|
const MIN_EXTRACTED_BYTES = 256 * 1024 * 1024;
|
||||||
*/
|
const MAX_ENTRIES = 250_000;
|
||||||
|
|
||||||
|
type SizeBudget = { used: number; max: number };
|
||||||
|
|
||||||
export async function extractZip(
|
export async function extractZip(
|
||||||
source: string,
|
source: string,
|
||||||
target: string,
|
target: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return extractZipInternal(source, target, true);
|
const { size: compressedSize } = await fs.promises.stat(source);
|
||||||
|
const max = Math.max(
|
||||||
|
compressedSize * COMPRESSION_HEADROOM,
|
||||||
|
MIN_EXTRACTED_BYTES,
|
||||||
|
);
|
||||||
|
return extractZipInternal(source, target, true, { used: 0, max });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal helper to extract a ZIP, with optional single-nested-ZIP handling.
|
|
||||||
* @param source Path to the ZIP file
|
|
||||||
* @param target Directory to extract into
|
|
||||||
* @param allowNested Whether to check and unwrap one level of nested ZIP
|
|
||||||
*/
|
|
||||||
function extractZipInternal(
|
function extractZipInternal(
|
||||||
source: string,
|
source: string,
|
||||||
target: string,
|
target: string,
|
||||||
allowNested: boolean,
|
allowNested: boolean,
|
||||||
|
budget: SizeBudget,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
yauzl.open(
|
yauzl.open(
|
||||||
source,
|
source,
|
||||||
{ lazyEntries: true, decodeStrings: false, autoClose: true },
|
{
|
||||||
|
lazyEntries: true,
|
||||||
|
decodeStrings: false,
|
||||||
|
autoClose: true,
|
||||||
|
validateEntrySizes: true,
|
||||||
|
},
|
||||||
(err, zipfile) => {
|
(err, zipfile) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
|
|
||||||
@@ -73,6 +81,15 @@ function extractZipInternal(
|
|||||||
? source.slice(0, -4) + '.inner.zip'
|
? source.slice(0, -4) + '.inner.zip'
|
||||||
: source + '.inner.zip';
|
: source + '.inner.zip';
|
||||||
|
|
||||||
|
budget.used += entry.uncompressedSize;
|
||||||
|
if (budget.used > budget.max) {
|
||||||
|
return reject(
|
||||||
|
new Error(
|
||||||
|
'Import archive exceeds the allowed extracted size limit',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
zipfile.openReadStream(entry, (openErr, rs) => {
|
zipfile.openReadStream(entry, (openErr, rs) => {
|
||||||
if (openErr) return reject(openErr);
|
if (openErr) return reject(openErr);
|
||||||
const ws = fs.createWriteStream(nestedPath);
|
const ws = fs.createWriteStream(nestedPath);
|
||||||
@@ -80,7 +97,7 @@ function extractZipInternal(
|
|||||||
ws.on('error', reject);
|
ws.on('error', reject);
|
||||||
ws.on('finish', () => {
|
ws.on('finish', () => {
|
||||||
zipfile.close();
|
zipfile.close();
|
||||||
extractZipInternal(nestedPath, target, false)
|
extractZipInternal(nestedPath, target, false, budget)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
fs.unlinkSync(nestedPath);
|
fs.unlinkSync(nestedPath);
|
||||||
resolve();
|
resolve();
|
||||||
@@ -91,13 +108,21 @@ function extractZipInternal(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
zipfile.close();
|
zipfile.close();
|
||||||
extractZipInternal(source, target, false).then(resolve, reject);
|
extractZipInternal(source, target, false, budget).then(
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
zipfile.once('error', reject);
|
zipfile.once('error', reject);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (zipfile.entryCount > MAX_ENTRIES) {
|
||||||
|
zipfile.close();
|
||||||
|
return reject(new Error('Import archive has too many entries'));
|
||||||
|
}
|
||||||
|
|
||||||
// Normal extraction
|
// Normal extraction
|
||||||
zipfile.readEntry();
|
zipfile.readEntry();
|
||||||
zipfile.on('entry', (entry) => {
|
zipfile.on('entry', (entry) => {
|
||||||
@@ -143,6 +168,15 @@ function extractZipInternal(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
budget.used += entry.uncompressedSize;
|
||||||
|
if (budget.used > budget.max) {
|
||||||
|
return reject(
|
||||||
|
new Error(
|
||||||
|
'Import archive exceeds the allowed extracted size limit',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle files
|
// Handle files
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
|||||||
Generated
+5
-11
@@ -772,8 +772,8 @@ importers:
|
|||||||
specifier: 8.21.0
|
specifier: 8.21.0
|
||||||
version: 8.21.0
|
version: 8.21.0
|
||||||
yauzl:
|
yauzl:
|
||||||
specifier: 3.2.1
|
specifier: 3.4.0
|
||||||
version: 3.2.1
|
version: 3.4.0
|
||||||
zod:
|
zod:
|
||||||
specifier: 4.3.6
|
specifier: 4.3.6
|
||||||
version: 4.3.6
|
version: 4.3.6
|
||||||
@@ -5606,9 +5606,6 @@ packages:
|
|||||||
bser@2.1.1:
|
bser@2.1.1:
|
||||||
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
||||||
|
|
||||||
buffer-crc32@0.2.13:
|
|
||||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1:
|
buffer-equal-constant-time@1.0.1:
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
@@ -10305,8 +10302,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
yauzl@3.2.1:
|
yauzl@3.4.0:
|
||||||
resolution: {integrity: sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==}
|
resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
yjs@13.6.30:
|
yjs@13.6.30:
|
||||||
@@ -15833,8 +15830,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-int64: 0.4.0
|
node-int64: 0.4.0
|
||||||
|
|
||||||
buffer-crc32@0.2.13: {}
|
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
@@ -21154,9 +21149,8 @@ snapshots:
|
|||||||
y18n: 5.0.8
|
y18n: 5.0.8
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
|
|
||||||
yauzl@3.2.1:
|
yauzl@3.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-crc32: 0.2.13
|
|
||||||
pend: 1.2.0
|
pend: 1.2.0
|
||||||
|
|
||||||
yjs@13.6.30:
|
yjs@13.6.30:
|
||||||
|
|||||||
Reference in New Issue
Block a user