@@ -5,6 +5,33 @@ const { execFile } = require('child_process');
5
5
const { promisify } = require ( 'util' ) ;
6
6
const execFileAsync = promisify ( execFile ) ;
7
7
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
+ / ^ ( g i t : \/ \/ | h t t p s : \/ \/ | s s h : \/ \/ | [ a - z A - Z 0 - 9 . _ % + - ] + @ [ a - z A - Z 0 - 9 . - ] + \. [ a - z A - 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 - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 \- _ / . ] * $ / ;
33
+ return typeof branch === 'string' && validBranchPattern . test ( branch ) ;
34
+ }
8
35
9
36
class ConfigLoader extends EventEmitter {
10
37
constructor ( initialConfig ) {
@@ -20,6 +47,12 @@ class ConfigLoader extends EventEmitter {
20
47
return ;
21
48
}
22
49
50
+ // Clear any existing interval before starting a new one
51
+ if ( this . reloadTimer ) {
52
+ clearInterval ( this . reloadTimer ) ;
53
+ this . reloadTimer = null ;
54
+ }
55
+
23
56
// Start periodic reload if interval is set
24
57
if ( configurationSources . reloadIntervalSeconds > 0 ) {
25
58
this . reloadTimer = setInterval (
@@ -92,6 +125,9 @@ class ConfigLoader extends EventEmitter {
92
125
93
126
async loadFromFile ( source ) {
94
127
const configPath = path . resolve ( process . cwd ( ) , source . path ) ;
128
+ if ( ! isValidPath ( configPath ) ) {
129
+ throw new Error ( 'Invalid configuration file path' ) ;
130
+ }
95
131
const content = await fs . promises . readFile ( configPath , 'utf8' ) ;
96
132
return JSON . parse ( content ) ;
97
133
}
@@ -108,24 +144,40 @@ class ConfigLoader extends EventEmitter {
108
144
109
145
async loadFromGit ( source ) {
110
146
// 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 ' ) ;
113
149
}
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 ' ) ;
116
152
}
117
153
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
+ }
119
160
await fs . promises . mkdir ( tempDir , { recursive : true } ) ;
120
161
121
162
const repoDir = path . join ( tempDir , Buffer . from ( source . repository ) . toString ( 'base64' ) ) ;
163
+ if ( ! isValidPath ( repoDir ) ) {
164
+ throw new Error ( 'Invalid repository directory path' ) ;
165
+ }
122
166
123
167
// Clone or pull repository
124
168
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 ) ;
129
181
} else {
130
182
await execFileAsync ( 'git' , [ 'pull' ] , { cwd : repoDir } ) ;
131
183
}
@@ -137,6 +189,9 @@ class ConfigLoader extends EventEmitter {
137
189
138
190
// Read and parse config file
139
191
const configPath = path . join ( repoDir , source . path ) ;
192
+ if ( ! isValidPath ( configPath ) ) {
193
+ throw new Error ( 'Invalid configuration file path in repository' ) ;
194
+ }
140
195
const content = await fs . promises . readFile ( configPath , 'utf8' ) ;
141
196
return JSON . parse ( content ) ;
142
197
}
0 commit comments