Skip to content

Commit 8d161a6

Browse files
committed
fix: adds failsafe checking for directory location and structure
1 parent 90e8878 commit 8d161a6

File tree

3 files changed

+83
-14
lines changed

3 files changed

+83
-14
lines changed

package-lock.json

Lines changed: 18 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"connect-mongo": "^5.1.0",
4747
"cors": "^2.8.5",
4848
"diff2html": "^3.4.33",
49+
"env-paths": "^3.0.0",
4950
"express": "^4.18.2",
5051
"express-http-proxy": "^2.0.0",
5152
"express-rate-limit": "^7.1.5",

src/config/ConfigLoader.js

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ const { execFile } = require('child_process');
55
const { promisify } = require('util');
66
const execFileAsync = promisify(execFile);
77
const EventEmitter = require('events');
8+
const envPaths = require('env-paths');
9+
10+
// Add path validation helper
11+
function isValidPath(filePath) {
12+
const resolvedPath = path.resolve(filePath);
13+
const cwd = process.cwd();
14+
return resolvedPath.startsWith(cwd);
15+
}
16+
17+
// Add URL validation helper
18+
function isValidGitUrl(url) {
19+
// Allow git://, https://, or ssh:// URLs
20+
// Also allow scp-style URLs (user@host:path)
21+
const validUrlPattern =
22+
/^(git:\/\/|https:\/\/|ssh:\/\/|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}:)/;
23+
return typeof url === 'string' && validUrlPattern.test(url);
24+
}
25+
26+
// Add branch name validation helper
27+
function isValidBranchName(branch) {
28+
// Branch names can contain alphanumeric, -, _, /, and .
29+
// Cannot start with - or .
30+
// Cannot contain consecutive dots
31+
// Cannot contain control characters or spaces
32+
const validBranchPattern = /^[a-zA-Z0-9][a-zA-Z0-9\-_/.]*$/;
33+
return typeof branch === 'string' && validBranchPattern.test(branch);
34+
}
835

936
class ConfigLoader extends EventEmitter {
1037
constructor(initialConfig) {
@@ -20,6 +47,12 @@ class ConfigLoader extends EventEmitter {
2047
return;
2148
}
2249

50+
// Clear any existing interval before starting a new one
51+
if (this.reloadTimer) {
52+
clearInterval(this.reloadTimer);
53+
this.reloadTimer = null;
54+
}
55+
2356
// Start periodic reload if interval is set
2457
if (configurationSources.reloadIntervalSeconds > 0) {
2558
this.reloadTimer = setInterval(
@@ -92,6 +125,9 @@ class ConfigLoader extends EventEmitter {
92125

93126
async loadFromFile(source) {
94127
const configPath = path.resolve(process.cwd(), source.path);
128+
if (!isValidPath(configPath)) {
129+
throw new Error('Invalid configuration file path');
130+
}
95131
const content = await fs.promises.readFile(configPath, 'utf8');
96132
return JSON.parse(content);
97133
}
@@ -108,24 +144,40 @@ class ConfigLoader extends EventEmitter {
108144

109145
async loadFromGit(source) {
110146
// Validate inputs
111-
if (!source.repository || typeof source.repository !== 'string') {
112-
throw new Error('Invalid repository URL');
147+
if (!source.repository || !isValidGitUrl(source.repository)) {
148+
throw new Error('Invalid repository URL format');
113149
}
114-
if (source.branch && typeof source.branch !== 'string') {
115-
throw new Error('Invalid branch name');
150+
if (source.branch && !isValidBranchName(source.branch)) {
151+
throw new Error('Invalid branch name format');
116152
}
117153

118-
const tempDir = path.join(process.cwd(), '.git-config-cache');
154+
// Use OS-specific cache directory
155+
const paths = envPaths('git-proxy', { suffix: '' });
156+
const tempDir = path.join(paths.cache, 'git-config-cache');
157+
if (!isValidPath(tempDir)) {
158+
throw new Error('Invalid temporary directory path');
159+
}
119160
await fs.promises.mkdir(tempDir, { recursive: true });
120161

121162
const repoDir = path.join(tempDir, Buffer.from(source.repository).toString('base64'));
163+
if (!isValidPath(repoDir)) {
164+
throw new Error('Invalid repository directory path');
165+
}
122166

123167
// Clone or pull repository
124168
if (!fs.existsSync(repoDir)) {
125-
if (source.auth?.type === 'ssh') {
126-
process.env.GIT_SSH_COMMAND = `ssh -i ${source.auth.privateKeyPath}`;
127-
}
128-
await execFileAsync('git', ['clone', source.repository, repoDir]);
169+
const execOptions = {
170+
cwd: process.cwd(),
171+
env: {
172+
...process.env,
173+
...(source.auth?.type === 'ssh'
174+
? {
175+
GIT_SSH_COMMAND: `ssh -i ${source.auth.privateKeyPath}`,
176+
}
177+
: {}),
178+
},
179+
};
180+
await execFileAsync('git', ['clone', source.repository, repoDir], execOptions);
129181
} else {
130182
await execFileAsync('git', ['pull'], { cwd: repoDir });
131183
}
@@ -137,6 +189,9 @@ class ConfigLoader extends EventEmitter {
137189

138190
// Read and parse config file
139191
const configPath = path.join(repoDir, source.path);
192+
if (!isValidPath(configPath)) {
193+
throw new Error('Invalid configuration file path in repository');
194+
}
140195
const content = await fs.promises.readFile(configPath, 'utf8');
141196
return JSON.parse(content);
142197
}

0 commit comments

Comments
 (0)