From 0895ffd17d8e3e9f2796e3c16e95fa4a79c8839a Mon Sep 17 00:00:00 2001 From: RinZ27 <222222878+RinZ27@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:21:08 +0700 Subject: [PATCH] fix(server): sanitize repo name to prevent argument injection --- gitnexus/src/server/git-clone.ts | 20 ++++++++++++++++++-- gitnexus/src/storage/git.ts | 16 +++++++++++++--- gitnexus/test/unit/git-clone.test.ts | 15 +++++++++++++++ gitnexus/tsconfig.json | 2 +- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/gitnexus/src/server/git-clone.ts b/gitnexus/src/server/git-clone.ts index 0f7bc2653..e34c6f84b 100644 --- a/gitnexus/src/server/git-clone.ts +++ b/gitnexus/src/server/git-clone.ts @@ -14,8 +14,24 @@ import { isIP } from 'net'; /** Extract the repository name from a git URL (HTTPS or SSH). */ export function extractRepoName(url: string): string { const cleaned = url.replace(/\/+$/, ''); - const lastSegment = cleaned.split(/[/:]/).pop() || 'unknown'; - return lastSegment.replace(/\.git$/, ''); + + // For SSH URLs like git@github.com:user/repo.git, we need to handle the colon. + // For HTTPS URLs, the only colon should be in the protocol. + let lastSegment: string; + if (cleaned.includes('@') && cleaned.includes(':') && !cleaned.startsWith('http')) { + // Likely an SSH URL + lastSegment = cleaned.split(/[:/]/).pop() || 'unknown'; + } else { + // Likely an HTTPS URL or local path + lastSegment = cleaned.split('/').pop() || 'unknown'; + } + + const name = lastSegment.replace(/\.git$/, ''); + + // Sanitize the name: + // 1. Prevent argument injection by stripping leading dashes. + // 2. Remove characters that are unsafe for directory names across platforms. + return name.replace(/^-+/, '').replace(/[<>:"/\\|?*]/g, '_') || 'unknown'; } /** Get the clone target directory for a repo name. */ diff --git a/gitnexus/src/storage/git.ts b/gitnexus/src/storage/git.ts index 8e0d6e555..bb4763167 100644 --- a/gitnexus/src/storage/git.ts +++ b/gitnexus/src/storage/git.ts @@ -151,9 +151,19 @@ export const parseRepoNameFromUrl = (url: string | null | undefined): string | n if (!trimmed) return null; // Strip `.git` suffix (case-insensitive) and any trailing slashes. const withoutSuffix = trimmed.replace(/\.git\/*$/i, '').replace(/\/+$/, ''); - // Last path segment, splitting on either `/` or `:` (covers SSH form). - const m = withoutSuffix.match(/[/:]([^/:]+)$/); - const candidate = m ? m[1] : withoutSuffix; + + // Last path segment, handling colons for SSH URLs. + // For HTTPS URLs, the only colon should be in the protocol. + let candidate: string; + if (withoutSuffix.includes('@') && withoutSuffix.includes(':') && !withoutSuffix.startsWith('http')) { + // Likely an SSH URL + const m = withoutSuffix.match(/[/:]([^/:]+)$/); + candidate = m ? m[1] : withoutSuffix; + } else { + // Likely an HTTPS URL or local path + candidate = withoutSuffix.split('/').pop() || withoutSuffix; + } + return candidate || null; }; diff --git a/gitnexus/test/unit/git-clone.test.ts b/gitnexus/test/unit/git-clone.test.ts index 832f95641..965a59001 100644 --- a/gitnexus/test/unit/git-clone.test.ts +++ b/gitnexus/test/unit/git-clone.test.ts @@ -22,6 +22,21 @@ describe('git-clone', () => { it('handles nested paths', () => { expect(extractRepoName('https://gitlab.com/group/subgroup/repo.git')).toBe('repo'); }); + + it('strips leading dashes to prevent argument injection', () => { + expect(extractRepoName('https://github.com/user/--upload-pack=payload.git')).toBe('upload-pack=payload'); + expect(extractRepoName('https://github.com/user/-repo')).toBe('repo'); + }); + + it('sanitizes unsafe directory characters', () => { + expect(extractRepoName('https://github.com/user/repo.git')).toBe('repo_tag_'); + expect(extractRepoName('https://github.com/user/repo:name')).toBe('repo_name'); + }); + + it('handles colons in SSH URLs correctly', () => { + expect(extractRepoName('git@github.com:user/my-repo.git')).toBe('my-repo'); + expect(extractRepoName('git@github.com:my-repo.git')).toBe('my-repo'); + }); }); describe('getCloneDir', () => { diff --git a/gitnexus/tsconfig.json b/gitnexus/tsconfig.json index 6b82c6d7d..ae64617ec 100644 --- a/gitnexus/tsconfig.json +++ b/gitnexus/tsconfig.json @@ -14,5 +14,5 @@ "declaration": true, "types": ["node"] }, - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] }