mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(api): validate analyze path from raw segments
Reject explicit '..' path segments in /api/analyze and share validation logic with unit tests to prevent behavior drift. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51aba4ac98
commit
d2622ed67e
2 changed files with 30 additions and 52 deletions
|
|
@ -105,6 +105,19 @@ export const isAllowedOrigin = (origin: string | undefined): boolean => {
|
|||
return false;
|
||||
};
|
||||
|
||||
export const validateAnalyzePath = (repoLocalPath: string): string | null => {
|
||||
if (!path.isAbsolute(repoLocalPath)) {
|
||||
return '"path" must be an absolute path';
|
||||
}
|
||||
|
||||
const pathSegments = repoLocalPath.split(/[\\/]+/);
|
||||
if (pathSegments.includes('..')) {
|
||||
return '"path" must not contain traversal sequences';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildGraph = async (
|
||||
includeContent = false,
|
||||
): Promise<{ nodes: GraphNode[]; relationships: GraphRelationship[] }> => {
|
||||
|
|
@ -864,30 +877,11 @@ export const createServer = async (port: number, host: string = '127.0.0.1') =>
|
|||
return;
|
||||
}
|
||||
|
||||
// Path validation: require absolute path, reject traversal (e.g. /tmp/../etc/passwd)
|
||||
// while still accepting common absolute-path variants with trailing separators.
|
||||
// Path validation: require absolute path and reject explicit traversal segments.
|
||||
if (repoLocalPath) {
|
||||
if (!path.isAbsolute(repoLocalPath)) {
|
||||
res.status(400).json({ error: '"path" must be an absolute path' });
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
// Preserve root paths, including Windows drive roots like C:\\
|
||||
if (p === path.parse(p).root || /^[A-Za-z]:[\\/]?$/.test(p)) return p;
|
||||
return p.replace(/[\\/]+$/, '');
|
||||
};
|
||||
|
||||
if (
|
||||
stripTrailingSeparator(normalizedInput) !== stripTrailingSeparator(resolvedInput)
|
||||
) {
|
||||
res.status(400).json({ error: '"path" must not contain traversal sequences' });
|
||||
const pathError = validateAnalyzePath(repoLocalPath);
|
||||
if (pathError) {
|
||||
res.status(400).json({ error: pathError });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,27 @@
|
|||
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);
|
||||
};
|
||||
import { validateAnalyzePath } from '../../src/server/api.js';
|
||||
|
||||
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);
|
||||
expect(validateAnalyzePath('/home/user/project/')).toBeNull();
|
||||
expect(validateAnalyzePath('/home/user/project//')).toBeNull();
|
||||
});
|
||||
|
||||
it('accepts normalized absolute paths', () => {
|
||||
expect(hasTraversalSequence('/home/user/project')).toBe(false);
|
||||
expect(validateAnalyzePath('/home/user/project')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects traversal sequences', () => {
|
||||
expect(hasTraversalSequence('/tmp/project/../other')).toBe(true);
|
||||
it('rejects traversal segments from raw input', () => {
|
||||
expect(validateAnalyzePath('/tmp/project/../other')).toBe(
|
||||
'"path" must not contain traversal sequences',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects relative paths', () => {
|
||||
expect(validateAnalyzePath('tmp/project')).toBe('"path" must be an absolute path');
|
||||
});
|
||||
|
||||
it('preserves Windows drive root semantics when stripping separators', () => {
|
||||
const normalizedRoot = path.win32.normalize('C:\\');
|
||||
const resolvedRoot = path.win32.resolve('C:\\');
|
||||
|
||||
const stripTrailingSeparator = (p: string): string => {
|
||||
if (p.length <= 1) return p;
|
||||
if (p === path.win32.parse(p).root || /^[A-Za-z]:[\\/]?$/.test(p)) return p;
|
||||
return p.replace(/[\\/]+$/, '');
|
||||
};
|
||||
|
||||
expect(stripTrailingSeparator(normalizedRoot)).toBe('C:\\');
|
||||
expect(stripTrailingSeparator(resolvedRoot)).toBe('C:\\');
|
||||
it('accepts Windows drive roots', () => {
|
||||
expect(validateAnalyzePath('C:\\')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue