@xuda.io/drive_module 1.1.1502 → 1.1.1503

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.
Files changed (2) hide show
  1. package/index.mjs +29 -3
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1746,6 +1746,22 @@ export const extract_drive_file = async (req, job_id, headers) => {
1746
1746
  const ext = path.extname(fileName).toLowerCase();
1747
1747
 
1748
1748
  if (fileName.startsWith('__MACOSX/')) {
1749
+ entry.autodrain();
1750
+ return;
1751
+ }
1752
+
1753
+ // A zip entry name is fully attacker controlled, and this validator used to be
1754
+ // called further down only to decide is_archive, never to reject. That let an entry named
1755
+ // "x.png$(cmd)" through as the doc's file_ext, and file_ext is what ocr_file_to_tmp
1756
+ // appends to the path handed to tesseract, which builds a shell command string.
1757
+ // Directories carry no extension, so only file entries are validated. A rejected
1758
+ // entry has to be drained rather than left unread, or unzipper stalls on the rest
1759
+ // of the archive.
1760
+ const validation_ret = type === 'Directory' ? { valid: false } : file_upload_validator(fileName);
1761
+
1762
+ if (type !== 'Directory' && !validation_ret.valid) {
1763
+ console.warn(`[drive_module] skipped zip entry with unsupported extension: ${path.basename(fileName)}`);
1764
+ entry.autodrain();
1749
1765
  return;
1750
1766
  }
1751
1767
 
@@ -1771,8 +1787,6 @@ export const extract_drive_file = async (req, job_id, headers) => {
1771
1787
  entry.pipe(fs.createWriteStream(extract_path));
1772
1788
  }
1773
1789
 
1774
- const validation_ret = file_upload_validator(fileName);
1775
-
1776
1790
  let file_doc = {
1777
1791
  _id: file_doc_id,
1778
1792
 
@@ -2490,9 +2504,21 @@ const detectLanguageByCountry = (cfCountry) => {
2490
2504
  // HTTP entirely. Only a drive with no bucket entry (a datacenter workspace drive, whose
2491
2505
  // files are scp'd to the hosting box) still needs the round trip, and that one now uses a
2492
2506
  // URL built the same way the drive listing builds it.
2507
+ // The readers below hand this path to a shell: node-tesseract-ocr joins a command string
2508
+ // and calls child_process.exec, and its double quotes around the path still let $(...) and
2509
+ // backticks expand. The upload validator already rejects a filename whose extension is not
2510
+ // on the allowlist, but this is the sink, so it does not get to assume that. Anything that
2511
+ // is not a plain short alphanumeric extension is dropped instead of passed through; the
2512
+ // readers key off the doc's mime, not this suffix, so losing it costs nothing.
2513
+ const SAFE_OCR_EXT = /^\.[a-z0-9]{1,12}$/;
2514
+ const safe_ocr_ext = (doc) => {
2515
+ const candidate = String(doc.file_ext || path.extname(doc.originalname || '') || '').toLowerCase();
2516
+ return SAFE_OCR_EXT.test(candidate) ? candidate : '';
2517
+ };
2518
+
2493
2519
  const ocr_file_to_tmp = async (app_id, doc) => {
2494
2520
  const write_tmp = async (buffer) => {
2495
- const tmp_path = path.join(os.tmpdir(), `xuda_ocr_${await _common.xuda_get_uuid('tmp')}${doc.file_ext || path.extname(doc.originalname || '') || ''}`);
2521
+ const tmp_path = path.join(os.tmpdir(), `xuda_ocr_${await _common.xuda_get_uuid('tmp')}${safe_ocr_ext(doc)}`);
2496
2522
  await fs.promises.writeFile(tmp_path, buffer);
2497
2523
  return tmp_path;
2498
2524
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/drive_module",
3
- "version": "1.1.1502",
3
+ "version": "1.1.1503",
4
4
  "description": "Xuda Drive Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {