isomorphic-git 1.38.5 → 1.38.6

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/index.cjs CHANGED
@@ -2231,7 +2231,13 @@ class GitRefManager {
2231
2231
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
2232
2232
  * @returns {Promise<string>} - The resolved object ID.
2233
2233
  */
2234
- static async resolve({ fs, gitdir, ref, depth = undefined }) {
2234
+ static async resolve({
2235
+ fs,
2236
+ gitdir,
2237
+ ref,
2238
+ depth = undefined,
2239
+ visited = new Set(),
2240
+ }) {
2235
2241
  if (depth !== undefined) {
2236
2242
  depth--;
2237
2243
  if (depth === -1) {
@@ -2242,7 +2248,7 @@ class GitRefManager {
2242
2248
  // Is it a ref pointer?
2243
2249
  if (ref.startsWith('ref: ')) {
2244
2250
  ref = ref.slice('ref: '.length);
2245
- return GitRefManager.resolve({ fs, gitdir, ref, depth })
2251
+ return GitRefManager.resolve({ fs, gitdir, ref, depth, visited })
2246
2252
  }
2247
2253
  // Is it a complete and valid SHA?
2248
2254
  if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
@@ -2261,7 +2267,21 @@ class GitRefManager {
2261
2267
  packedMap.get(ref)
2262
2268
  );
2263
2269
  if (sha) {
2264
- return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
2270
+ // Guard against circular symbolic references (e.g. a -> b -> a). Without
2271
+ // tracking visited refs this recursion would not terminate.
2272
+ if (visited.has(ref)) {
2273
+ throw new InternalError(
2274
+ `Circular reference detected while resolving ref "${ref}"`
2275
+ )
2276
+ }
2277
+ visited.add(ref);
2278
+ return GitRefManager.resolve({
2279
+ fs,
2280
+ gitdir,
2281
+ ref: sha.trim(),
2282
+ depth,
2283
+ visited,
2284
+ })
2265
2285
  }
2266
2286
  }
2267
2287
  // Do we give up?
@@ -2517,8 +2537,25 @@ function parseBuffer(buffer) {
2517
2537
  const type = mode2type$1(mode);
2518
2538
  const path = buffer.slice(space + 1, nullchar).toString('utf8');
2519
2539
 
2520
- // Prevent malicious git repos from writing to "..\foo" on clone etc
2521
- if (path.includes('\\') || path.includes('/')) {
2540
+ // Reject reserved entry names, matching git's verify_path(): path separators,
2541
+ // "." and ".." (which resolve outside the working directory), and ".git" (which git
2542
+ // disallows as a path component). Checks mirror git's is_ntfs_dotgit() and
2543
+ // is_hfs_dotgit(): case-insensitive, trailing dots/spaces stripped (NTFS),
2544
+ // NTFS 8.3 short name aliases (git~1..git~9), and HFS+ ignorable Unicode
2545
+ // characters stripped (zero-width joiners, directional marks, BOM, etc.).
2546
+ const hfsClean = path.replace(
2547
+ /[\u200C-\u200F\u202A-\u202E\u206A-\u206F\uFEFF]/g,
2548
+ ''
2549
+ );
2550
+ const normalized = hfsClean.toLowerCase().replace(/[. ]+$/, '');
2551
+ if (
2552
+ path.includes('\\') ||
2553
+ path.includes('/') ||
2554
+ hfsClean === '.' ||
2555
+ hfsClean === '..' ||
2556
+ normalized === '.git' ||
2557
+ /^\.?git~[1-9]$/.test(normalized)
2558
+ ) {
2522
2559
  throw new UnsafeFilepathError(path)
2523
2560
  }
2524
2561
 
@@ -2692,21 +2729,26 @@ function applyDelta(delta, source) {
2692
2729
  if (firstOp.byteLength === targetSize) {
2693
2730
  target = firstOp;
2694
2731
  } else {
2695
- // Otherwise, allocate a fresh buffer and slices
2696
- target = Buffer.alloc(targetSize);
2697
- const writer = new BufferCursor(target);
2698
- writer.copy(firstOp);
2732
+ // Build the result from the delta ops instead of pre-allocating `targetSize`.
2733
+ // `targetSize` comes from the delta header and may not match the actual op output,
2734
+ // so allocating it up front can reserve far more memory than the ops produce.
2735
+ // Building from the ops bounds memory to the real output size; the size check below
2736
+ // still validates the total before concatenating.
2737
+ const chunks = [firstOp];
2738
+ let tell = firstOp.byteLength;
2699
2739
 
2700
2740
  while (!reader.eof()) {
2701
- writer.copy(readOp(reader, source));
2741
+ const op = readOp(reader, source);
2742
+ chunks.push(op);
2743
+ tell += op.byteLength;
2702
2744
  }
2703
2745
 
2704
- const tell = writer.tell();
2705
2746
  if (targetSize !== tell) {
2706
2747
  throw new InternalError(
2707
2748
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
2708
2749
  )
2709
2750
  }
2751
+ target = Buffer.concat(chunks, targetSize);
2710
2752
  }
2711
2753
  return target
2712
2754
  }
@@ -6855,6 +6897,30 @@ const worthWalking = (filepath, root) => {
6855
6897
 
6856
6898
  // @ts-check
6857
6899
 
6900
+ /**
6901
+ * Throw if any leading directory component of `fullpath` (relative to `dir`) is a symbolic
6902
+ * link. git does not follow symlinks in the leading path when writing working-tree files;
6903
+ * matching that avoids writing through a symlinked parent into a location outside `dir`.
6904
+ *
6905
+ * @param {import('../models/FileSystem.js').FileSystem} fs
6906
+ * @param {string} dir
6907
+ * @param {string} fullpath
6908
+ */
6909
+ async function assertNoSymlinkInLeadingPath(fs, dir, fullpath) {
6910
+ const parts = fullpath.split('/');
6911
+ parts.pop(); // the final segment is the entry being written, not a leading dir
6912
+ let current = dir;
6913
+ for (const part of parts) {
6914
+ if (part === '' || part === '.') continue
6915
+ current = `${current}/${part}`;
6916
+ const stats = await fs.lstat(current);
6917
+ // lstat returns null when the path doesn't exist yet (nothing to traverse).
6918
+ if (stats && stats.isSymbolicLink()) {
6919
+ throw new UnsafeFilepathError(fullpath)
6920
+ }
6921
+ }
6922
+ }
6923
+
6858
6924
  /**
6859
6925
  * @param {object} args
6860
6926
  * @param {import('../models/FileSystem.js').FileSystem} args.fs
@@ -7061,6 +7127,7 @@ async function _checkout({
7061
7127
  .filter(([method]) => method === 'mkdir' || method === 'mkdir-index')
7062
7128
  .map(async function ([_, fullpath]) {
7063
7129
  const filepath = `${dir}/${fullpath}`;
7130
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7064
7131
  await fs.mkdir(filepath);
7065
7132
  if (onProgress) {
7066
7133
  await onProgress({
@@ -7130,6 +7197,10 @@ async function _checkout({
7130
7197
  .map(async function ([method, fullpath, oid, mode, chmod]) {
7131
7198
  const filepath = `${dir}/${fullpath}`;
7132
7199
  if (method !== 'create-index' && method !== 'mkdir-index') {
7200
+ // Don't write through a symlinked leading path: a parent component that
7201
+ // is a symlink could otherwise redirect the write outside the working
7202
+ // tree. git applies the same check (it does not follow symlinks here).
7203
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7133
7204
  const { object } = await _readObject({
7134
7205
  fs,
7135
7206
  cache,
@@ -7536,6 +7607,7 @@ async function updateWorkingDir(
7536
7607
  ) {
7537
7608
  const filepath = `${dir}/${fullpath}`;
7538
7609
  if (method !== 'create-index' && method !== 'mkdir-index') {
7610
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7539
7611
  const { object } = await _readObject({ fs, cache, gitdir, oid });
7540
7612
  if (chmod) {
7541
7613
  await fs.rm(filepath);
@@ -9410,8 +9482,8 @@ function filterCapabilities(server, client) {
9410
9482
 
9411
9483
  const pkg = {
9412
9484
  name: 'isomorphic-git',
9413
- version: '1.38.5',
9414
- agent: 'git/isomorphic-git@1.38.5',
9485
+ version: '1.38.6',
9486
+ agent: 'git/isomorphic-git@1.38.6',
9415
9487
  };
9416
9488
 
9417
9489
  class FIFO {
package/index.js CHANGED
@@ -2224,7 +2224,13 @@ class GitRefManager {
2224
2224
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
2225
2225
  * @returns {Promise<string>} - The resolved object ID.
2226
2226
  */
2227
- static async resolve({ fs, gitdir, ref, depth = undefined }) {
2227
+ static async resolve({
2228
+ fs,
2229
+ gitdir,
2230
+ ref,
2231
+ depth = undefined,
2232
+ visited = new Set(),
2233
+ }) {
2228
2234
  if (depth !== undefined) {
2229
2235
  depth--;
2230
2236
  if (depth === -1) {
@@ -2235,7 +2241,7 @@ class GitRefManager {
2235
2241
  // Is it a ref pointer?
2236
2242
  if (ref.startsWith('ref: ')) {
2237
2243
  ref = ref.slice('ref: '.length);
2238
- return GitRefManager.resolve({ fs, gitdir, ref, depth })
2244
+ return GitRefManager.resolve({ fs, gitdir, ref, depth, visited })
2239
2245
  }
2240
2246
  // Is it a complete and valid SHA?
2241
2247
  if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
@@ -2254,7 +2260,21 @@ class GitRefManager {
2254
2260
  packedMap.get(ref)
2255
2261
  );
2256
2262
  if (sha) {
2257
- return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
2263
+ // Guard against circular symbolic references (e.g. a -> b -> a). Without
2264
+ // tracking visited refs this recursion would not terminate.
2265
+ if (visited.has(ref)) {
2266
+ throw new InternalError(
2267
+ `Circular reference detected while resolving ref "${ref}"`
2268
+ )
2269
+ }
2270
+ visited.add(ref);
2271
+ return GitRefManager.resolve({
2272
+ fs,
2273
+ gitdir,
2274
+ ref: sha.trim(),
2275
+ depth,
2276
+ visited,
2277
+ })
2258
2278
  }
2259
2279
  }
2260
2280
  // Do we give up?
@@ -2510,8 +2530,25 @@ function parseBuffer(buffer) {
2510
2530
  const type = mode2type$1(mode);
2511
2531
  const path = buffer.slice(space + 1, nullchar).toString('utf8');
2512
2532
 
2513
- // Prevent malicious git repos from writing to "..\foo" on clone etc
2514
- if (path.includes('\\') || path.includes('/')) {
2533
+ // Reject reserved entry names, matching git's verify_path(): path separators,
2534
+ // "." and ".." (which resolve outside the working directory), and ".git" (which git
2535
+ // disallows as a path component). Checks mirror git's is_ntfs_dotgit() and
2536
+ // is_hfs_dotgit(): case-insensitive, trailing dots/spaces stripped (NTFS),
2537
+ // NTFS 8.3 short name aliases (git~1..git~9), and HFS+ ignorable Unicode
2538
+ // characters stripped (zero-width joiners, directional marks, BOM, etc.).
2539
+ const hfsClean = path.replace(
2540
+ /[\u200C-\u200F\u202A-\u202E\u206A-\u206F\uFEFF]/g,
2541
+ ''
2542
+ );
2543
+ const normalized = hfsClean.toLowerCase().replace(/[. ]+$/, '');
2544
+ if (
2545
+ path.includes('\\') ||
2546
+ path.includes('/') ||
2547
+ hfsClean === '.' ||
2548
+ hfsClean === '..' ||
2549
+ normalized === '.git' ||
2550
+ /^\.?git~[1-9]$/.test(normalized)
2551
+ ) {
2515
2552
  throw new UnsafeFilepathError(path)
2516
2553
  }
2517
2554
 
@@ -2685,21 +2722,26 @@ function applyDelta(delta, source) {
2685
2722
  if (firstOp.byteLength === targetSize) {
2686
2723
  target = firstOp;
2687
2724
  } else {
2688
- // Otherwise, allocate a fresh buffer and slices
2689
- target = Buffer.alloc(targetSize);
2690
- const writer = new BufferCursor(target);
2691
- writer.copy(firstOp);
2725
+ // Build the result from the delta ops instead of pre-allocating `targetSize`.
2726
+ // `targetSize` comes from the delta header and may not match the actual op output,
2727
+ // so allocating it up front can reserve far more memory than the ops produce.
2728
+ // Building from the ops bounds memory to the real output size; the size check below
2729
+ // still validates the total before concatenating.
2730
+ const chunks = [firstOp];
2731
+ let tell = firstOp.byteLength;
2692
2732
 
2693
2733
  while (!reader.eof()) {
2694
- writer.copy(readOp(reader, source));
2734
+ const op = readOp(reader, source);
2735
+ chunks.push(op);
2736
+ tell += op.byteLength;
2695
2737
  }
2696
2738
 
2697
- const tell = writer.tell();
2698
2739
  if (targetSize !== tell) {
2699
2740
  throw new InternalError(
2700
2741
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
2701
2742
  )
2702
2743
  }
2744
+ target = Buffer.concat(chunks, targetSize);
2703
2745
  }
2704
2746
  return target
2705
2747
  }
@@ -6842,6 +6884,30 @@ const worthWalking = (filepath, root) => {
6842
6884
 
6843
6885
  // @ts-check
6844
6886
 
6887
+ /**
6888
+ * Throw if any leading directory component of `fullpath` (relative to `dir`) is a symbolic
6889
+ * link. git does not follow symlinks in the leading path when writing working-tree files;
6890
+ * matching that avoids writing through a symlinked parent into a location outside `dir`.
6891
+ *
6892
+ * @param {import('../models/FileSystem.js').FileSystem} fs
6893
+ * @param {string} dir
6894
+ * @param {string} fullpath
6895
+ */
6896
+ async function assertNoSymlinkInLeadingPath(fs, dir, fullpath) {
6897
+ const parts = fullpath.split('/');
6898
+ parts.pop(); // the final segment is the entry being written, not a leading dir
6899
+ let current = dir;
6900
+ for (const part of parts) {
6901
+ if (part === '' || part === '.') continue
6902
+ current = `${current}/${part}`;
6903
+ const stats = await fs.lstat(current);
6904
+ // lstat returns null when the path doesn't exist yet (nothing to traverse).
6905
+ if (stats && stats.isSymbolicLink()) {
6906
+ throw new UnsafeFilepathError(fullpath)
6907
+ }
6908
+ }
6909
+ }
6910
+
6845
6911
  /**
6846
6912
  * @param {object} args
6847
6913
  * @param {import('../models/FileSystem.js').FileSystem} args.fs
@@ -7048,6 +7114,7 @@ async function _checkout({
7048
7114
  .filter(([method]) => method === 'mkdir' || method === 'mkdir-index')
7049
7115
  .map(async function ([_, fullpath]) {
7050
7116
  const filepath = `${dir}/${fullpath}`;
7117
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7051
7118
  await fs.mkdir(filepath);
7052
7119
  if (onProgress) {
7053
7120
  await onProgress({
@@ -7117,6 +7184,10 @@ async function _checkout({
7117
7184
  .map(async function ([method, fullpath, oid, mode, chmod]) {
7118
7185
  const filepath = `${dir}/${fullpath}`;
7119
7186
  if (method !== 'create-index' && method !== 'mkdir-index') {
7187
+ // Don't write through a symlinked leading path: a parent component that
7188
+ // is a symlink could otherwise redirect the write outside the working
7189
+ // tree. git applies the same check (it does not follow symlinks here).
7190
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7120
7191
  const { object } = await _readObject({
7121
7192
  fs,
7122
7193
  cache,
@@ -7523,6 +7594,7 @@ async function updateWorkingDir(
7523
7594
  ) {
7524
7595
  const filepath = `${dir}/${fullpath}`;
7525
7596
  if (method !== 'create-index' && method !== 'mkdir-index') {
7597
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7526
7598
  const { object } = await _readObject({ fs, cache, gitdir, oid });
7527
7599
  if (chmod) {
7528
7600
  await fs.rm(filepath);
@@ -9397,8 +9469,8 @@ function filterCapabilities(server, client) {
9397
9469
 
9398
9470
  const pkg = {
9399
9471
  name: 'isomorphic-git',
9400
- version: '1.38.5',
9401
- agent: 'git/isomorphic-git@1.38.5',
9472
+ version: '1.38.6',
9473
+ agent: 'git/isomorphic-git@1.38.6',
9402
9474
  };
9403
9475
 
9404
9476
  class FIFO {