From aaa14b9fc0b9b51c4fb9d1dba40fada7cbbe0356 Mon Sep 17 00:00:00 2001 From: simosmik Date: Tue, 10 Mar 2026 21:16:24 +0000 Subject: [PATCH] fix: codeql user value provided path validation --- server/routes/git.js | 45 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/server/routes/git.js b/server/routes/git.js index 2214d902..24a8b828 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -61,10 +61,19 @@ function validateBranchName(branch) { return branch; } -function validateFilePath(file) { +function validateFilePath(file, projectPath) { if (!file || file.includes('\0')) { throw new Error('Invalid file path'); } + // Prevent path traversal: resolve the file relative to the project root + // and ensure the result stays within the project directory + if (projectPath) { + const resolved = path.resolve(projectPath, file); + const normalizedRoot = path.resolve(projectPath) + path.sep; + if (!resolved.startsWith(normalizedRoot) && resolved !== path.resolve(projectPath)) { + throw new Error('Invalid file path: path traversal detected'); + } + } return file; } @@ -75,15 +84,33 @@ function validateRemoteName(remote) { return remote; } +function validateProjectPath(projectPath) { + if (!projectPath || projectPath.includes('\0')) { + throw new Error('Invalid project path'); + } + const resolved = path.resolve(projectPath); + // Must be an absolute path after resolution + if (!path.isAbsolute(resolved)) { + throw new Error('Invalid project path: must be absolute'); + } + // Block obviously dangerous paths + if (resolved === '/' || resolved === path.sep) { + throw new Error('Invalid project path: root directory not allowed'); + } + return resolved; +} + // Helper function to get the actual project path from the encoded project name async function getActualProjectPath(projectName) { + let projectPath; try { - return await extractProjectDirectory(projectName); + projectPath = await extractProjectDirectory(projectName); } catch (error) { console.error(`Error extracting project directory for ${projectName}:`, error); // Fallback to the old method - return projectName.replace(/-/g, '/'); + projectPath = projectName.replace(/-/g, '/'); } + return validateProjectPath(projectPath); } // Helper function to strip git diff headers @@ -230,7 +257,7 @@ router.get('/diff', async (req, res) => { await validateGitRepository(projectPath); // Validate file path - validateFilePath(file); + validateFilePath(file, projectPath); // Check if file is untracked or deleted const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath }); @@ -295,7 +322,7 @@ router.get('/file-with-diff', async (req, res) => { await validateGitRepository(projectPath); // Validate file path - validateFilePath(file); + validateFilePath(file, projectPath); // Check file status const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath }); @@ -406,7 +433,7 @@ router.post('/commit', async (req, res) => { // Stage selected files for (const file of files) { - validateFilePath(file); + validateFilePath(file, projectPath); await spawnAsync('git', ['add', file], { cwd: projectPath }); } @@ -610,7 +637,7 @@ router.post('/generate-commit-message', async (req, res) => { let diffContext = ''; for (const file of files) { try { - validateFilePath(file); + validateFilePath(file, projectPath); const { stdout } = await spawnAsync( 'git', ['diff', 'HEAD', '--', file], { cwd: projectPath } @@ -1139,7 +1166,7 @@ router.post('/discard', async (req, res) => { await validateGitRepository(projectPath); // Validate file path - validateFilePath(file); + validateFilePath(file, projectPath); // Check file status to determine correct discard command const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath }); @@ -1188,7 +1215,7 @@ router.post('/delete-untracked', async (req, res) => { await validateGitRepository(projectPath); // Validate file path - validateFilePath(file); + validateFilePath(file, projectPath); // Check if file is actually untracked const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain', file], { cwd: projectPath });