Skip to content

Commit 0b7f9ab

Browse files
authored
Merge pull request finos#1355 from jescalada/1336-fix-invalid-regex-commitConfig
fix: add invalid regex validation for `commitConfig`
2 parents 588d7f3 + 07da631 commit 0b7f9ab

File tree

7 files changed

+757
-48
lines changed

7 files changed

+757
-48
lines changed

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ if (argv.v) {
4848
process.exit(0);
4949
}
5050

51-
console.log('validating config');
51+
console.log('Validating config');
5252
validate();
5353

5454
console.log('Setting up the proxy and Service');

src/config/ConfigLoader.ts

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { execFile } from 'child_process';
55
import { promisify } from 'util';
66
import { EventEmitter } from 'events';
77
import envPaths from 'env-paths';
8-
import { GitProxyConfig, Convert } from './generated/config';
8+
import { GitProxyConfig } from './generated/config';
99
import { Configuration, ConfigurationSource, FileSource, HttpSource, GitSource } from './types';
10+
import { loadConfig, validateConfig } from './validators';
1011

1112
const execFileAsync = promisify(execFile);
1213

@@ -148,7 +149,7 @@ export class ConfigLoader extends EventEmitter {
148149
);
149150
console.log(`Found ${enabledSources.length} enabled configuration sources`);
150151

151-
const configs = await Promise.all(
152+
const loadedConfigs = await Promise.all(
152153
enabledSources.map(async (source: ConfigurationSource) => {
153154
try {
154155
console.log(`Loading configuration from ${source.type} source`);
@@ -161,10 +162,12 @@ export class ConfigLoader extends EventEmitter {
161162
);
162163

163164
// Filter out null results from failed loads
164-
const validConfigs = configs.filter((config): config is GitProxyConfig => config !== null);
165+
const nonNullConfigs = loadedConfigs.filter(
166+
(config): config is GitProxyConfig => config !== null,
167+
);
165168

166-
if (validConfigs.length === 0) {
167-
console.log('No valid configurations loaded from any source');
169+
if (nonNullConfigs.length === 0) {
170+
console.log('All loaded configurations are empty, skipping reload');
168171
return;
169172
}
170173

@@ -173,15 +176,20 @@ export class ConfigLoader extends EventEmitter {
173176
console.log(`Using ${shouldMerge ? 'merge' : 'override'} strategy for configuration`);
174177

175178
const newConfig = shouldMerge
176-
? validConfigs.reduce(
179+
? nonNullConfigs.reduce(
177180
(acc, curr) => {
178181
return this.deepMerge(acc, curr) as Configuration;
179182
},
180183
{ ...this.config },
181184
)
182-
: { ...this.config, ...validConfigs[validConfigs.length - 1] }; // Use last config for override
185+
: { ...this.config, ...nonNullConfigs[nonNullConfigs.length - 1] }; // Use last config for override
186+
187+
if (!validateConfig(newConfig)) {
188+
console.error('Invalid configuration, skipping reload');
189+
return;
190+
}
183191

184-
// Emit change event if config changed
192+
// Emit change event if config changed and is valid
185193
if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) {
186194
console.log('Configuration has changed, updating and emitting change event');
187195
this.config = newConfig;
@@ -218,16 +226,7 @@ export class ConfigLoader extends EventEmitter {
218226
throw new Error('Invalid configuration file path');
219227
}
220228
console.log(`Loading configuration from file: ${configPath}`);
221-
const content = await fs.promises.readFile(configPath, 'utf8');
222-
223-
// Use QuickType to validate and parse the configuration
224-
try {
225-
return Convert.toGitProxyConfig(content);
226-
} catch (error) {
227-
throw new Error(
228-
`Invalid configuration file format: ${error instanceof Error ? error.message : 'Unknown error'}`,
229-
);
230-
}
229+
return loadConfig(`file: ${configPath}`, async () => fs.promises.readFile(configPath, 'utf8'));
231230
}
232231

233232
async loadFromHttp(source: HttpSource): Promise<GitProxyConfig> {
@@ -237,18 +236,10 @@ export class ConfigLoader extends EventEmitter {
237236
...(source.auth?.type === 'bearer' ? { Authorization: `Bearer ${source.auth.token}` } : {}),
238237
};
239238

240-
const response = await axios.get(source.url, { headers });
241-
242-
// Use QuickType to validate and parse the configuration from HTTP response
243-
try {
244-
const configJson =
245-
typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
246-
return Convert.toGitProxyConfig(configJson);
247-
} catch (error) {
248-
throw new Error(
249-
`Invalid configuration format from HTTP source: ${error instanceof Error ? error.message : 'Unknown error'}`,
250-
);
251-
}
239+
return loadConfig(`HTTP: ${source.url}`, async () => {
240+
const response = await axios.get(source.url, { headers });
241+
return typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
242+
});
252243
}
253244

254245
async loadFromGit(source: GitSource): Promise<GitProxyConfig> {
@@ -344,17 +335,7 @@ export class ConfigLoader extends EventEmitter {
344335
throw new Error(`Configuration file not found at ${configPath}`);
345336
}
346337

347-
try {
348-
const content = await fs.promises.readFile(configPath, 'utf8');
349-
350-
// Use QuickType to validate and parse the configuration from Git
351-
const config = Convert.toGitProxyConfig(content);
352-
console.log('Configuration loaded successfully from Git');
353-
return config;
354-
} catch (error: any) {
355-
console.error('Failed to read or parse configuration file:', error.message);
356-
throw new Error(`Failed to read or parse configuration file: ${error.message}`);
357-
}
338+
return loadConfig(`git: ${configPath}`, async () => fs.promises.readFile(configPath, 'utf8'));
358339
}
359340

360341
deepMerge(target: Record<string, any>, source: Record<string, any>): Record<string, any> {

src/config/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ConfigLoader } from './ConfigLoader';
66
import { Configuration } from './types';
77
import { serverConfig } from './env';
88
import { getConfigFile } from './file';
9+
import { validateConfig } from './validators';
910

1011
// Cache for current configuration
1112
let _currentConfig: GitProxyConfig | null = null;
@@ -69,6 +70,15 @@ function loadFullConfiguration(): GitProxyConfig {
6970

7071
_currentConfig = mergeConfigurations(defaultConfig, userSettings);
7172

73+
if (!validateConfig(_currentConfig)) {
74+
console.error(
75+
'Invalid configuration: Please check your configuration file and restart GitProxy.',
76+
);
77+
throw new Error(
78+
'Invalid configuration: Please check your configuration file and restart GitProxy.',
79+
);
80+
}
81+
7282
return _currentConfig;
7383
}
7484

@@ -325,7 +335,7 @@ const handleConfigUpdate = async (newConfig: Configuration) => {
325335

326336
// Initialize config loader
327337
function initializeConfigLoader() {
328-
const config = loadFullConfiguration() as Configuration;
338+
const config = loadFullConfiguration();
329339
_configLoader = new ConfigLoader(config);
330340

331341
// Handle configuration updates
@@ -352,7 +362,6 @@ export const reloadConfiguration = async () => {
352362

353363
// Initialize configuration on module load
354364
try {
355-
loadFullConfiguration();
356365
initializeConfigLoader();
357366
console.log('Configuration loaded successfully');
358367
} catch (error) {

src/config/validators.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Convert, GitProxyConfig } from './generated/config';
2+
3+
const validationChain = [validateCommitConfig];
4+
5+
/**
6+
* Executes all custom validators on the configuration
7+
* @param config The configuration to validate
8+
* @returns true if the configuration is valid, false otherwise
9+
*/
10+
export const validateConfig = (config: GitProxyConfig): boolean => {
11+
return validationChain.every((validator) => validator(config));
12+
};
13+
14+
/**
15+
* Validates that commit configuration uses valid regular expressions.
16+
* @param config The commit configuration to validate
17+
* @returns true if the commit configuration is valid, false otherwise
18+
*/
19+
function validateCommitConfig(config: GitProxyConfig): boolean {
20+
return (
21+
validateConfigRegex(config, 'commitConfig.author.email.local.block') &&
22+
validateConfigRegex(config, 'commitConfig.author.email.domain.allow') &&
23+
validateConfigRegex(config, 'commitConfig.message.block.patterns') &&
24+
validateConfigRegex(config, 'commitConfig.diff.block.patterns') &&
25+
validateConfigRegex(config, 'commitConfig.diff.block.providers')
26+
);
27+
}
28+
29+
/**
30+
* Validates that a regular expression is valid.
31+
* @param pattern The regular expression to validate
32+
* @param context The context of the regular expression
33+
* @returns true if the regular expression is valid, false otherwise
34+
*/
35+
function isValidRegex(pattern: string, context: string): boolean {
36+
try {
37+
new RegExp(pattern);
38+
return true;
39+
} catch {
40+
console.error(`Invalid regular expression for ${context}: ${pattern}`);
41+
return false;
42+
}
43+
}
44+
45+
/**
46+
* Validates that a value in the configuration is a valid regular expression.
47+
* @param config The configuration to validate
48+
* @param path The path to the value to validate
49+
* @returns true if the value is a valid regular expression, false otherwise
50+
*/
51+
function validateConfigRegex(config: GitProxyConfig, path: string): boolean {
52+
const getValueAtPath = (obj: unknown, path: string): unknown => {
53+
return path.split('.').reduce((current, key) => {
54+
if (current == null || typeof current !== 'object') {
55+
return undefined;
56+
}
57+
return (current as Record<string, unknown>)[key];
58+
}, obj);
59+
};
60+
61+
const value = getValueAtPath(config, path);
62+
63+
if (!value) return true;
64+
65+
if (typeof value === 'string') {
66+
return isValidRegex(value, path);
67+
}
68+
69+
if (Array.isArray(value)) {
70+
for (const pattern of value) {
71+
if (!isValidRegex(pattern, path)) return false;
72+
}
73+
return true;
74+
}
75+
76+
if (typeof value === 'object') {
77+
return Object.values(value).every((pattern) => isValidRegex(pattern as string, path));
78+
}
79+
80+
return true;
81+
}
82+
83+
/**
84+
* Loads and parses a GitProxyConfig object from a given context and loading strategy.
85+
* @param context The context of the configuration
86+
* @param loader The loading strategy to use
87+
* @returns The parsed GitProxyConfig object
88+
*/
89+
export async function loadConfig(
90+
context: string,
91+
loader: () => Promise<string>,
92+
): Promise<GitProxyConfig> {
93+
const raw = await loader();
94+
return parseGitProxyConfig(raw, context);
95+
}
96+
97+
/**
98+
* Parses a raw string into a GitProxyConfig object.
99+
* @param raw The raw string to parse
100+
* @param context The context of the configuration
101+
* @returns The parsed GitProxyConfig object
102+
*/
103+
function parseGitProxyConfig(raw: string, context: string): GitProxyConfig {
104+
try {
105+
return Convert.toGitProxyConfig(raw);
106+
} catch (error) {
107+
throw new Error(
108+
`Invalid configuration format in ${context}: ${error instanceof Error ? error.message : 'Unknown error'}`,
109+
);
110+
}
111+
}

0 commit comments

Comments
 (0)