isomorphic-git 1.38.5 → 1.38.7

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,31 @@ 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), NTFS Alternate Data Streams, and
2545
+ // HFS+ ignorable Unicode characters stripped (zero-width joiners, directional
2546
+ // marks, BOM, etc.).
2547
+ const hfsClean = path.replace(
2548
+ /[\u200C-\u200F\u202A-\u202E\u206A-\u206F\uFEFF]/g,
2549
+ ''
2550
+ );
2551
+ // NTFS Alternate Data Streams are referenced as `<name>:<stream>:<type>`, and a
2552
+ // directory's default stream lets `.git::$INDEX_ALLOCATION` resolve to the `.git`
2553
+ // directory itself. git forbids *any* ADS, so only the portion before the first
2554
+ // colon names the actual entry we normalize and check.
2555
+ const ntfsClean = hfsClean.split(':')[0];
2556
+ const normalized = ntfsClean.toLowerCase().replace(/[. ]+$/, '');
2557
+ if (
2558
+ path.includes('\\') ||
2559
+ path.includes('/') ||
2560
+ hfsClean === '.' ||
2561
+ hfsClean === '..' ||
2562
+ normalized === '.git' ||
2563
+ /^\.?git~[1-9]$/.test(normalized)
2564
+ ) {
2522
2565
  throw new UnsafeFilepathError(path)
2523
2566
  }
2524
2567
 
@@ -2692,21 +2735,26 @@ function applyDelta(delta, source) {
2692
2735
  if (firstOp.byteLength === targetSize) {
2693
2736
  target = firstOp;
2694
2737
  } else {
2695
- // Otherwise, allocate a fresh buffer and slices
2696
- target = Buffer.alloc(targetSize);
2697
- const writer = new BufferCursor(target);
2698
- writer.copy(firstOp);
2738
+ // Build the result from the delta ops instead of pre-allocating `targetSize`.
2739
+ // `targetSize` comes from the delta header and may not match the actual op output,
2740
+ // so allocating it up front can reserve far more memory than the ops produce.
2741
+ // Building from the ops bounds memory to the real output size; the size check below
2742
+ // still validates the total before concatenating.
2743
+ const chunks = [firstOp];
2744
+ let tell = firstOp.byteLength;
2699
2745
 
2700
2746
  while (!reader.eof()) {
2701
- writer.copy(readOp(reader, source));
2747
+ const op = readOp(reader, source);
2748
+ chunks.push(op);
2749
+ tell += op.byteLength;
2702
2750
  }
2703
2751
 
2704
- const tell = writer.tell();
2705
2752
  if (targetSize !== tell) {
2706
2753
  throw new InternalError(
2707
2754
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
2708
2755
  )
2709
2756
  }
2757
+ target = Buffer.concat(chunks, targetSize);
2710
2758
  }
2711
2759
  return target
2712
2760
  }
@@ -6855,6 +6903,30 @@ const worthWalking = (filepath, root) => {
6855
6903
 
6856
6904
  // @ts-check
6857
6905
 
6906
+ /**
6907
+ * Throw if any leading directory component of `fullpath` (relative to `dir`) is a symbolic
6908
+ * link. git does not follow symlinks in the leading path when writing working-tree files;
6909
+ * matching that avoids writing through a symlinked parent into a location outside `dir`.
6910
+ *
6911
+ * @param {import('../models/FileSystem.js').FileSystem} fs
6912
+ * @param {string} dir
6913
+ * @param {string} fullpath
6914
+ */
6915
+ async function assertNoSymlinkInLeadingPath(fs, dir, fullpath) {
6916
+ const parts = fullpath.split('/');
6917
+ parts.pop(); // the final segment is the entry being written, not a leading dir
6918
+ let current = dir;
6919
+ for (const part of parts) {
6920
+ if (part === '' || part === '.') continue
6921
+ current = `${current}/${part}`;
6922
+ const stats = await fs.lstat(current);
6923
+ // lstat returns null when the path doesn't exist yet (nothing to traverse).
6924
+ if (stats && stats.isSymbolicLink()) {
6925
+ throw new UnsafeFilepathError(fullpath)
6926
+ }
6927
+ }
6928
+ }
6929
+
6858
6930
  /**
6859
6931
  * @param {object} args
6860
6932
  * @param {import('../models/FileSystem.js').FileSystem} args.fs
@@ -7061,6 +7133,7 @@ async function _checkout({
7061
7133
  .filter(([method]) => method === 'mkdir' || method === 'mkdir-index')
7062
7134
  .map(async function ([_, fullpath]) {
7063
7135
  const filepath = `${dir}/${fullpath}`;
7136
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7064
7137
  await fs.mkdir(filepath);
7065
7138
  if (onProgress) {
7066
7139
  await onProgress({
@@ -7130,6 +7203,10 @@ async function _checkout({
7130
7203
  .map(async function ([method, fullpath, oid, mode, chmod]) {
7131
7204
  const filepath = `${dir}/${fullpath}`;
7132
7205
  if (method !== 'create-index' && method !== 'mkdir-index') {
7206
+ // Don't write through a symlinked leading path: a parent component that
7207
+ // is a symlink could otherwise redirect the write outside the working
7208
+ // tree. git applies the same check (it does not follow symlinks here).
7209
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7133
7210
  const { object } = await _readObject({
7134
7211
  fs,
7135
7212
  cache,
@@ -7536,6 +7613,7 @@ async function updateWorkingDir(
7536
7613
  ) {
7537
7614
  const filepath = `${dir}/${fullpath}`;
7538
7615
  if (method !== 'create-index' && method !== 'mkdir-index') {
7616
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7539
7617
  const { object } = await _readObject({ fs, cache, gitdir, oid });
7540
7618
  if (chmod) {
7541
7619
  await fs.rm(filepath);
@@ -9410,8 +9488,8 @@ function filterCapabilities(server, client) {
9410
9488
 
9411
9489
  const pkg = {
9412
9490
  name: 'isomorphic-git',
9413
- version: '1.38.5',
9414
- agent: 'git/isomorphic-git@1.38.5',
9491
+ version: '1.38.7',
9492
+ agent: 'git/isomorphic-git@1.38.7',
9415
9493
  };
9416
9494
 
9417
9495
  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,31 @@ 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), NTFS Alternate Data Streams, and
2538
+ // HFS+ ignorable Unicode characters stripped (zero-width joiners, directional
2539
+ // marks, BOM, etc.).
2540
+ const hfsClean = path.replace(
2541
+ /[\u200C-\u200F\u202A-\u202E\u206A-\u206F\uFEFF]/g,
2542
+ ''
2543
+ );
2544
+ // NTFS Alternate Data Streams are referenced as `<name>:<stream>:<type>`, and a
2545
+ // directory's default stream lets `.git::$INDEX_ALLOCATION` resolve to the `.git`
2546
+ // directory itself. git forbids *any* ADS, so only the portion before the first
2547
+ // colon names the actual entry we normalize and check.
2548
+ const ntfsClean = hfsClean.split(':')[0];
2549
+ const normalized = ntfsClean.toLowerCase().replace(/[. ]+$/, '');
2550
+ if (
2551
+ path.includes('\\') ||
2552
+ path.includes('/') ||
2553
+ hfsClean === '.' ||
2554
+ hfsClean === '..' ||
2555
+ normalized === '.git' ||
2556
+ /^\.?git~[1-9]$/.test(normalized)
2557
+ ) {
2515
2558
  throw new UnsafeFilepathError(path)
2516
2559
  }
2517
2560
 
@@ -2685,21 +2728,26 @@ function applyDelta(delta, source) {
2685
2728
  if (firstOp.byteLength === targetSize) {
2686
2729
  target = firstOp;
2687
2730
  } else {
2688
- // Otherwise, allocate a fresh buffer and slices
2689
- target = Buffer.alloc(targetSize);
2690
- const writer = new BufferCursor(target);
2691
- writer.copy(firstOp);
2731
+ // Build the result from the delta ops instead of pre-allocating `targetSize`.
2732
+ // `targetSize` comes from the delta header and may not match the actual op output,
2733
+ // so allocating it up front can reserve far more memory than the ops produce.
2734
+ // Building from the ops bounds memory to the real output size; the size check below
2735
+ // still validates the total before concatenating.
2736
+ const chunks = [firstOp];
2737
+ let tell = firstOp.byteLength;
2692
2738
 
2693
2739
  while (!reader.eof()) {
2694
- writer.copy(readOp(reader, source));
2740
+ const op = readOp(reader, source);
2741
+ chunks.push(op);
2742
+ tell += op.byteLength;
2695
2743
  }
2696
2744
 
2697
- const tell = writer.tell();
2698
2745
  if (targetSize !== tell) {
2699
2746
  throw new InternalError(
2700
2747
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
2701
2748
  )
2702
2749
  }
2750
+ target = Buffer.concat(chunks, targetSize);
2703
2751
  }
2704
2752
  return target
2705
2753
  }
@@ -6842,6 +6890,30 @@ const worthWalking = (filepath, root) => {
6842
6890
 
6843
6891
  // @ts-check
6844
6892
 
6893
+ /**
6894
+ * Throw if any leading directory component of `fullpath` (relative to `dir`) is a symbolic
6895
+ * link. git does not follow symlinks in the leading path when writing working-tree files;
6896
+ * matching that avoids writing through a symlinked parent into a location outside `dir`.
6897
+ *
6898
+ * @param {import('../models/FileSystem.js').FileSystem} fs
6899
+ * @param {string} dir
6900
+ * @param {string} fullpath
6901
+ */
6902
+ async function assertNoSymlinkInLeadingPath(fs, dir, fullpath) {
6903
+ const parts = fullpath.split('/');
6904
+ parts.pop(); // the final segment is the entry being written, not a leading dir
6905
+ let current = dir;
6906
+ for (const part of parts) {
6907
+ if (part === '' || part === '.') continue
6908
+ current = `${current}/${part}`;
6909
+ const stats = await fs.lstat(current);
6910
+ // lstat returns null when the path doesn't exist yet (nothing to traverse).
6911
+ if (stats && stats.isSymbolicLink()) {
6912
+ throw new UnsafeFilepathError(fullpath)
6913
+ }
6914
+ }
6915
+ }
6916
+
6845
6917
  /**
6846
6918
  * @param {object} args
6847
6919
  * @param {import('../models/FileSystem.js').FileSystem} args.fs
@@ -7048,6 +7120,7 @@ async function _checkout({
7048
7120
  .filter(([method]) => method === 'mkdir' || method === 'mkdir-index')
7049
7121
  .map(async function ([_, fullpath]) {
7050
7122
  const filepath = `${dir}/${fullpath}`;
7123
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7051
7124
  await fs.mkdir(filepath);
7052
7125
  if (onProgress) {
7053
7126
  await onProgress({
@@ -7117,6 +7190,10 @@ async function _checkout({
7117
7190
  .map(async function ([method, fullpath, oid, mode, chmod]) {
7118
7191
  const filepath = `${dir}/${fullpath}`;
7119
7192
  if (method !== 'create-index' && method !== 'mkdir-index') {
7193
+ // Don't write through a symlinked leading path: a parent component that
7194
+ // is a symlink could otherwise redirect the write outside the working
7195
+ // tree. git applies the same check (it does not follow symlinks here).
7196
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7120
7197
  const { object } = await _readObject({
7121
7198
  fs,
7122
7199
  cache,
@@ -7523,6 +7600,7 @@ async function updateWorkingDir(
7523
7600
  ) {
7524
7601
  const filepath = `${dir}/${fullpath}`;
7525
7602
  if (method !== 'create-index' && method !== 'mkdir-index') {
7603
+ await assertNoSymlinkInLeadingPath(fs, dir, fullpath);
7526
7604
  const { object } = await _readObject({ fs, cache, gitdir, oid });
7527
7605
  if (chmod) {
7528
7606
  await fs.rm(filepath);
@@ -9397,8 +9475,8 @@ function filterCapabilities(server, client) {
9397
9475
 
9398
9476
  const pkg = {
9399
9477
  name: 'isomorphic-git',
9400
- version: '1.38.5',
9401
- agent: 'git/isomorphic-git@1.38.5',
9478
+ version: '1.38.7',
9479
+ agent: 'git/isomorphic-git@1.38.7',
9402
9480
  };
9403
9481
 
9404
9482
  class FIFO {