diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index a0e830d52..2c623d9c1 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -865,12 +865,26 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => } // Path validation: require absolute path, reject traversal (e.g. /tmp/../etc/passwd) + // while still accepting common absolute-path variants with trailing separators. if (repoLocalPath) { if (!path.isAbsolute(repoLocalPath)) { res.status(400).json({ error: '"path" must be an absolute path' }); return; } - if (path.normalize(repoLocalPath) !== path.resolve(repoLocalPath)) { + + const normalizedInput = path.normalize(repoLocalPath); + const resolvedInput = path.resolve(repoLocalPath); + // normalize() may keep a trailing separator (e.g. /home/user/project/) + // while resolve() typically strips it. Compare without trailing separators + // so valid absolute paths are accepted consistently. + const stripTrailingSeparator = (p: string): string => { + if (p.length <= 1) return p; + return p.replace(/[\\/]+$/, ''); + }; + + if ( + stripTrailingSeparator(normalizedInput) !== stripTrailingSeparator(resolvedInput) + ) { res.status(400).json({ error: '"path" must not contain traversal sequences' }); return; } diff --git a/gitnexus/test/unit/analyze-path-validation.test.ts b/gitnexus/test/unit/analyze-path-validation.test.ts new file mode 100644 index 000000000..fc732b338 --- /dev/null +++ b/gitnexus/test/unit/analyze-path-validation.test.ts @@ -0,0 +1,28 @@ +import path from 'path'; +import { describe, expect, it } from 'vitest'; + +const hasTraversalSequence = (repoLocalPath: string): boolean => { + const normalizedInput = path.normalize(repoLocalPath); + const resolvedInput = path.resolve(repoLocalPath); + const stripTrailingSeparator = (p: string): string => { + if (p.length <= 1) return p; + return p.replace(/[\\/]+$/, ''); + }; + + return stripTrailingSeparator(normalizedInput) !== stripTrailingSeparator(resolvedInput); +}; + +describe('analyze path validation', () => { + it('accepts absolute paths with trailing separator', () => { + expect(hasTraversalSequence('/home/user/project/')).toBe(false); + expect(hasTraversalSequence('/home/user/project//')).toBe(false); + }); + + it('accepts normalized absolute paths', () => { + expect(hasTraversalSequence('/home/user/project')).toBe(false); + }); + + it('rejects traversal sequences', () => { + expect(hasTraversalSequence('/tmp/project/../other')).toBe(true); + }); +});