gitnexus 1.6.5-rc.5 → 1.6.5-rc.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/dist/server/git-clone.js +10 -13
- package/dist/storage/git.d.ts +17 -6
- package/dist/storage/git.js +46 -12
- package/package.json +1 -1
package/dist/server/git-clone.js
CHANGED
|
@@ -10,6 +10,7 @@ import os from 'os';
|
|
|
10
10
|
import fs from 'fs/promises';
|
|
11
11
|
import { isIP } from 'net';
|
|
12
12
|
import { logger } from '../core/logger.js';
|
|
13
|
+
import { parseRepoNameFromUrl } from '../storage/git.js';
|
|
13
14
|
/** Root directory for all cloned repositories. Targets must resolve inside this. */
|
|
14
15
|
const CLONE_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
|
|
15
16
|
// A valid git repository name is filesystem-safe: alphanumerics plus `. _ -`.
|
|
@@ -25,19 +26,15 @@ const REPO_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
|
25
26
|
* clone root via path traversal.
|
|
26
27
|
*/
|
|
27
28
|
export function extractRepoName(url) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const cleaned = url.slice(0, end);
|
|
35
|
-
const lastSegment = cleaned.split(/[/:]/).pop() || '';
|
|
36
|
-
const stripped = lastSegment.endsWith('.git') ? lastSegment.slice(0, -4) : lastSegment;
|
|
37
|
-
if (!stripped || stripped === '.' || stripped === '..' || !REPO_NAME_PATTERN.test(stripped)) {
|
|
29
|
+
const name = parseRepoNameFromUrl(url);
|
|
30
|
+
if (!name ||
|
|
31
|
+
name === '.' ||
|
|
32
|
+
name === '..' ||
|
|
33
|
+
name === 'unknown' ||
|
|
34
|
+
!REPO_NAME_PATTERN.test(name)) {
|
|
38
35
|
throw new Error('Could not extract a valid repository name from URL');
|
|
39
36
|
}
|
|
40
|
-
return
|
|
37
|
+
return name;
|
|
41
38
|
}
|
|
42
39
|
/** Get the clone target directory for a repo name. */
|
|
43
40
|
export function getCloneDir(repoName) {
|
|
@@ -347,8 +344,8 @@ export async function cloneOrPull(url, targetDir, onProgress) {
|
|
|
347
344
|
throw new Error(`Clone target must be a subdirectory of ${CLONE_ROOT}`);
|
|
348
345
|
}
|
|
349
346
|
// Always validate the requested URL — the prior shape only ran this in
|
|
350
|
-
// the
|
|
351
|
-
//
|
|
347
|
+
// the code path where the repo was cloned. Now it runs unconditionally,
|
|
348
|
+
// preventing SSRF / blocked-host bypasses even when targetDir already exists.
|
|
352
349
|
validateGitUrl(url);
|
|
353
350
|
const exists = await fs.access(path.join(safeTarget, '.git')).then(() => true, () => false);
|
|
354
351
|
if (exists) {
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -108,13 +108,24 @@ export declare const hasGitDir: (dirPath: string) => boolean;
|
|
|
108
108
|
*/
|
|
109
109
|
export declare const getRemoteOriginUrl: (repoPath: string) => string | null;
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
|
|
114
|
-
* unparseable input.
|
|
111
|
+
* Sanitize a repository name to prevent argument injection and ensure
|
|
112
|
+
* cross-platform filesystem compatibility.
|
|
115
113
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
114
|
+
* 1. Strips leading dashes to prevent git command-line argument injection
|
|
115
|
+
* (e.g., --upload-pack=evil).
|
|
116
|
+
* 2. Replaces characters that are unsafe for directory names across
|
|
117
|
+
* platforms (Windows/macOS/Linux) with underscores.
|
|
118
|
+
* 3. Blocks path traversal segments ("." and "..") and Windows reserved
|
|
119
|
+
* names (e.g., CON, NUL) to prevent directory escape.
|
|
120
|
+
*/
|
|
121
|
+
export declare const sanitizeRepoName: (name: string) => string;
|
|
122
|
+
/**
|
|
123
|
+
* Parse a repository name out of a git remote URL. Handles common shapes
|
|
124
|
+
* including SSH (git@host:owner/repo.git) and HTTPS (https://host/owner/repo.git).
|
|
125
|
+
*
|
|
126
|
+
* Returns a sanitized, filesystem-safe name or null if no name could be inferred.
|
|
127
|
+
* Returning null (rather than 'unknown') allows callers to use ?? null-coalescing
|
|
128
|
+
* for fallbacks without risk of registry collisions on 'unknown'.
|
|
118
129
|
*/
|
|
119
130
|
export declare const parseRepoNameFromUrl: (url: string | null | undefined) => string | null;
|
|
120
131
|
/**
|
package/dist/storage/git.js
CHANGED
|
@@ -257,13 +257,36 @@ export const getRemoteOriginUrl = (repoPath) => {
|
|
|
257
257
|
}
|
|
258
258
|
};
|
|
259
259
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
|
|
263
|
-
* unparseable input.
|
|
260
|
+
* Sanitize a repository name to prevent argument injection and ensure
|
|
261
|
+
* cross-platform filesystem compatibility.
|
|
264
262
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
263
|
+
* 1. Strips leading dashes to prevent git command-line argument injection
|
|
264
|
+
* (e.g., --upload-pack=evil).
|
|
265
|
+
* 2. Replaces characters that are unsafe for directory names across
|
|
266
|
+
* platforms (Windows/macOS/Linux) with underscores.
|
|
267
|
+
* 3. Blocks path traversal segments ("." and "..") and Windows reserved
|
|
268
|
+
* names (e.g., CON, NUL) to prevent directory escape.
|
|
269
|
+
*/
|
|
270
|
+
export const sanitizeRepoName = (name) => {
|
|
271
|
+
// 1. Prevent argument injection by stripping leading dashes.
|
|
272
|
+
// 2. Remove characters that are not alphanumerics, dots, underscores, or dashes.
|
|
273
|
+
const sanitized = name.replace(/^-+/, '').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
274
|
+
// 3. Block path traversal segments and Windows reserved names.
|
|
275
|
+
// Windows reserved names like CON, PRN, AUX, NUL, COM1-9, LPT1-9 cannot
|
|
276
|
+
// be used as directory names on Windows even if they have an extension.
|
|
277
|
+
const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
|
|
278
|
+
if (!sanitized || sanitized === '.' || sanitized === '..' || reserved.test(sanitized)) {
|
|
279
|
+
return 'unknown';
|
|
280
|
+
}
|
|
281
|
+
return sanitized;
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Parse a repository name out of a git remote URL. Handles common shapes
|
|
285
|
+
* including SSH (git@host:owner/repo.git) and HTTPS (https://host/owner/repo.git).
|
|
286
|
+
*
|
|
287
|
+
* Returns a sanitized, filesystem-safe name or null if no name could be inferred.
|
|
288
|
+
* Returning null (rather than 'unknown') allows callers to use ?? null-coalescing
|
|
289
|
+
* for fallbacks without risk of registry collisions on 'unknown'.
|
|
267
290
|
*/
|
|
268
291
|
export const parseRepoNameFromUrl = (url) => {
|
|
269
292
|
if (!url)
|
|
@@ -271,12 +294,23 @@ export const parseRepoNameFromUrl = (url) => {
|
|
|
271
294
|
const trimmed = url.trim();
|
|
272
295
|
if (!trimmed)
|
|
273
296
|
return null;
|
|
274
|
-
// Strip
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
297
|
+
// Strip trailing slashes without a regex to avoid polynomial-ReDoS on
|
|
298
|
+
// pathological inputs like `https://x.com/y` + '/'.repeat(1e6).
|
|
299
|
+
let end = trimmed.length;
|
|
300
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 47 /* '/' */)
|
|
301
|
+
end--;
|
|
302
|
+
let cleaned = trimmed.slice(0, end);
|
|
303
|
+
// Strip trailing .git (case-insensitive)
|
|
304
|
+
if (cleaned.toLowerCase().endsWith('.git')) {
|
|
305
|
+
cleaned = cleaned.slice(0, -4);
|
|
306
|
+
}
|
|
307
|
+
// Last path segment, handling colons for SSH URLs and path traversal.
|
|
308
|
+
// Split on both / and : to consistently extract the last part.
|
|
309
|
+
const candidate = cleaned.split(/[/:]/).pop() || '';
|
|
310
|
+
if (!candidate)
|
|
311
|
+
return null;
|
|
312
|
+
const safe = sanitizeRepoName(candidate);
|
|
313
|
+
return safe === 'unknown' ? null : safe;
|
|
280
314
|
};
|
|
281
315
|
/**
|
|
282
316
|
* Convenience wrapper: derive a registry-friendly name from the repo's
|
package/package.json
CHANGED