Security Fixes: - Add command injection prevention in n8n-mcp wrapper with whitelist validation - Fix race condition in database initialization with proper lock directory creation - Add flock availability check with fallback behavior - Implement comprehensive input sanitization in parse-config.js Improvements: - Add debug logging support to parse-config.js (DEBUG_CONFIG=true) - Improve test cleanup error handling with proper error tracking - Increase integration test timeouts for CI compatibility - Update test assertions to check environment variables instead of processes All critical security vulnerabilities identified by code review have been addressed. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
187 lines
5.5 KiB
JavaScript
187 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Parse JSON config file and output shell-safe export commands
|
|
* Only outputs variables that aren't already set in environment
|
|
*
|
|
* Security: Uses safe quoting without any shell execution
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
|
|
// Debug logging support
|
|
const DEBUG = process.env.DEBUG_CONFIG === 'true';
|
|
|
|
function debugLog(message) {
|
|
if (DEBUG) {
|
|
process.stderr.write(`[parse-config] ${message}\n`);
|
|
}
|
|
}
|
|
|
|
const configPath = process.argv[2] || '/app/config.json';
|
|
debugLog(`Using config path: ${configPath}`);
|
|
|
|
// Dangerous environment variables that should never be set
|
|
const DANGEROUS_VARS = new Set([
|
|
'PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'LD_AUDIT',
|
|
'BASH_ENV', 'ENV', 'CDPATH', 'IFS', 'PS1', 'PS2', 'PS3', 'PS4',
|
|
'SHELL', 'BASH_FUNC', 'SHELLOPTS', 'GLOBIGNORE',
|
|
'PERL5LIB', 'PYTHONPATH', 'NODE_PATH', 'RUBYLIB'
|
|
]);
|
|
|
|
/**
|
|
* Sanitize a key name for use as environment variable
|
|
* Converts to uppercase and replaces invalid chars with underscore
|
|
*/
|
|
function sanitizeKey(key) {
|
|
// Convert to string and handle edge cases
|
|
const keyStr = String(key || '').trim();
|
|
|
|
if (!keyStr) {
|
|
return 'EMPTY_KEY';
|
|
}
|
|
|
|
const sanitized = keyStr
|
|
.toUpperCase()
|
|
.replace(/[^A-Z0-9]+/g, '_')
|
|
.replace(/^_+|_+$/g, '') // Trim underscores
|
|
.replace(/^(\d)/, '_$1'); // Prefix with _ if starts with number
|
|
|
|
// If sanitization results in empty string, use a default
|
|
return sanitized || 'EMPTY_KEY';
|
|
}
|
|
|
|
/**
|
|
* Safely quote a string for shell use
|
|
* This follows POSIX shell quoting rules
|
|
*/
|
|
function shellQuote(str) {
|
|
// Remove null bytes which are not allowed in environment variables
|
|
str = str.replace(/\x00/g, '');
|
|
|
|
// Always use single quotes for consistency and safety
|
|
// Single quotes protect everything except other single quotes
|
|
return "'" + str.replace(/'/g, "'\"'\"'") + "'";
|
|
}
|
|
|
|
try {
|
|
if (!fs.existsSync(configPath)) {
|
|
debugLog(`Config file not found at: ${configPath}`);
|
|
process.exit(0); // Silent exit if no config file
|
|
}
|
|
|
|
let configContent;
|
|
let config;
|
|
|
|
try {
|
|
configContent = fs.readFileSync(configPath, 'utf8');
|
|
debugLog(`Read config file, size: ${configContent.length} bytes`);
|
|
} catch (readError) {
|
|
// Silent exit on read errors
|
|
debugLog(`Error reading config: ${readError.message}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
try {
|
|
config = JSON.parse(configContent);
|
|
debugLog(`Parsed config with ${Object.keys(config).length} top-level keys`);
|
|
} catch (parseError) {
|
|
// Silent exit on invalid JSON
|
|
debugLog(`Error parsing JSON: ${parseError.message}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Validate config is an object
|
|
if (typeof config !== 'object' || config === null || Array.isArray(config)) {
|
|
// Silent exit on invalid config structure
|
|
process.exit(0);
|
|
}
|
|
|
|
// Convert nested objects to flat environment variables
|
|
const flattenConfig = (obj, prefix = '', depth = 0) => {
|
|
const result = {};
|
|
|
|
// Prevent infinite recursion
|
|
if (depth > 10) {
|
|
return result;
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const sanitizedKey = sanitizeKey(key);
|
|
|
|
// Skip if sanitization resulted in EMPTY_KEY (indicating invalid key)
|
|
if (sanitizedKey === 'EMPTY_KEY') {
|
|
debugLog(`Skipping key '${key}': invalid key name`);
|
|
continue;
|
|
}
|
|
|
|
const envKey = prefix ? `${prefix}_${sanitizedKey}` : sanitizedKey;
|
|
|
|
// Skip if key is too long
|
|
if (envKey.length > 255) {
|
|
debugLog(`Skipping key '${envKey}': too long (${envKey.length} chars)`);
|
|
continue;
|
|
}
|
|
|
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
// Recursively flatten nested objects
|
|
Object.assign(result, flattenConfig(value, envKey, depth + 1));
|
|
} else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
// Only include if not already set in environment
|
|
if (!process.env[envKey]) {
|
|
let stringValue = String(value);
|
|
|
|
// Handle special JavaScript number values
|
|
if (typeof value === 'number') {
|
|
if (!isFinite(value)) {
|
|
if (value === Infinity) {
|
|
stringValue = 'Infinity';
|
|
} else if (value === -Infinity) {
|
|
stringValue = '-Infinity';
|
|
} else if (isNaN(value)) {
|
|
stringValue = 'NaN';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip if value is too long
|
|
if (stringValue.length <= 32768) {
|
|
result[envKey] = stringValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// Output shell-safe export commands
|
|
const flattened = flattenConfig(config);
|
|
const exports = [];
|
|
|
|
for (const [key, value] of Object.entries(flattened)) {
|
|
// Validate key name (alphanumeric and underscore only)
|
|
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
|
|
continue; // Skip invalid variable names
|
|
}
|
|
|
|
// Skip dangerous variables
|
|
if (DANGEROUS_VARS.has(key) || key.startsWith('BASH_FUNC_')) {
|
|
debugLog(`Warning: Ignoring dangerous variable: ${key}`);
|
|
process.stderr.write(`Warning: Ignoring dangerous variable: ${key}\n`);
|
|
continue;
|
|
}
|
|
|
|
// Safely quote the value
|
|
const quotedValue = shellQuote(value);
|
|
exports.push(`export ${key}=${quotedValue}`);
|
|
}
|
|
|
|
// Use process.stdout.write to ensure output goes to stdout
|
|
if (exports.length > 0) {
|
|
process.stdout.write(exports.join('\n') + '\n');
|
|
}
|
|
|
|
} catch (error) {
|
|
// Silent fail - don't break the container startup
|
|
process.exit(0);
|
|
} |