vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
288 lines (287 loc) • 12.1 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import logger from '../../../logger.js';
import { getUnifiedSecurityConfig } from './unified-security-config.js';
export class PathSecurityValidator {
static instance = null;
config;
auditEvents = [];
auditCounter = 0;
constructor(config) {
try {
const unifiedConfig = getUnifiedSecurityConfig();
const unifiedPathConfig = unifiedConfig.getPathValidatorConfig();
this.config = {
allowedDirectories: unifiedPathConfig.allowedDirectories,
allowedExtensions: [
'.json', '.yaml', '.yml', '.txt', '.md', '.log', '.gz',
'.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte',
'.py', '.java', '.go', '.rs', '.cpp', '.c', '.h',
'.html', '.css', '.scss', '.sass', '.less',
'.xml', '.csv', '.sql', '.sh', '.bat', '.ps1'
],
blockedPatterns: [
/\.\./g,
/~\//g,
/\/etc\//g,
/\/proc\//g,
/\/sys\//g,
/\/dev\//g,
/\/var\/log\//g,
/\/root\//g,
/\/home\/[^\/]+\/\.[^\/]+/g,
/\0/g,
/[\x00-\x1f\x7f-\x9f]/g
],
allowSymlinks: false,
allowAbsolutePaths: true,
maxPathLength: unifiedPathConfig.maxPathLength,
...config
};
logger.info({
config: this.config,
source: 'unified-security-config'
}, 'Path Security Validator initialized from unified configuration');
}
catch (error) {
logger.warn({ err: error }, 'Unified security config not available, falling back to defaults');
this.config = {
allowedDirectories: [
process.cwd(),
path.join(process.cwd(), 'data'),
path.join(process.cwd(), 'src'),
path.join(process.cwd(), 'temp'),
'/tmp',
'/test'
],
allowedExtensions: [
'.json', '.yaml', '.yml', '.txt', '.md', '.log', '.gz',
'.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte',
'.py', '.java', '.go', '.rs', '.cpp', '.c', '.h',
'.html', '.css', '.scss', '.sass', '.less',
'.xml', '.csv', '.sql', '.sh', '.bat', '.ps1'
],
blockedPatterns: [
/\.\./g,
/~\//g,
/\/etc\//g,
/\/proc\//g,
/\/sys\//g,
/\/dev\//g,
/\/var\/log\//g,
/\/root\//g,
/\/home\/[^\/]+\/\.[^\/]+/g,
/\0/g,
/[\x00-\x1f\x7f-\x9f]/g
],
allowSymlinks: false,
allowAbsolutePaths: true,
maxPathLength: 4096,
...config
};
logger.info({
config: this.config,
source: 'hardcoded-defaults'
}, 'Path Security Validator initialized from defaults (fallback)');
}
}
static getInstance(config) {
if (!PathSecurityValidator.instance) {
PathSecurityValidator.instance = new PathSecurityValidator(config);
}
return PathSecurityValidator.instance;
}
async validatePath(filePath, _operation = 'read', context) {
const startTime = Date.now();
const auditInfo = {
originalPath: filePath,
timestamp: new Date(),
validationTime: 0
};
try {
if (!filePath || typeof filePath !== 'string') {
return this.createValidationResult(false, auditInfo, 'Invalid path input', 'malformed');
}
if (filePath.length > this.config.maxPathLength) {
return this.createValidationResult(false, auditInfo, 'Path too long', 'malformed');
}
const blockedPattern = this.config.blockedPatterns.find(pattern => pattern.test(filePath));
if (blockedPattern) {
return this.createValidationResult(false, auditInfo, 'Path contains blocked pattern', 'traversal');
}
let canonicalPath;
try {
canonicalPath = path.resolve(filePath);
}
catch (error) {
logger.debug({ error, filePath }, 'Path canonicalization failed');
return this.createValidationResult(false, auditInfo, 'Path canonicalization failed', 'malformed');
}
if (this.containsTraversal(canonicalPath, filePath)) {
return this.createValidationResult(false, auditInfo, 'Directory traversal detected', 'traversal');
}
if (!this.isPathInWhitelist(canonicalPath)) {
return this.createValidationResult(false, auditInfo, 'Path not in whitelist', 'whitelist');
}
if (path.extname(canonicalPath) && !this.isExtensionAllowed(canonicalPath)) {
return this.createValidationResult(false, auditInfo, 'File extension not allowed', 'whitelist');
}
if (!this.config.allowSymlinks && await this.isSymbolicLink(canonicalPath)) {
return this.createValidationResult(false, auditInfo, 'Symbolic links not allowed', 'symlink');
}
if (path.isAbsolute(filePath) && !this.config.allowAbsolutePaths) {
return this.createValidationResult(false, auditInfo, 'Absolute paths not allowed', 'absolute');
}
auditInfo.validationTime = Date.now() - startTime;
this.logAuditEvent('validation_success', filePath, canonicalPath, undefined, context, auditInfo.validationTime);
return {
valid: true,
canonicalPath,
securityViolation: false,
auditInfo
};
}
catch (error) {
auditInfo.validationTime = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
this.logAuditEvent('validation_failure', filePath, undefined, 'system_error', context, auditInfo.validationTime, error);
return this.createValidationResult(false, auditInfo, `Validation error: ${errorMessage}`, 'malformed');
}
}
createValidationResult(valid, auditInfo, error, violationType, canonicalPath) {
auditInfo.validationTime = Date.now() - auditInfo.timestamp.getTime();
if (!valid && violationType) {
this.logAuditEvent('security_violation', auditInfo.originalPath, canonicalPath, violationType, undefined, auditInfo.validationTime);
}
return {
valid,
canonicalPath,
error,
securityViolation: !valid && violationType !== 'malformed',
violationType,
auditInfo
};
}
containsTraversal(canonicalPath, originalPath) {
const traversalPatterns = ['../', '..\\', '%2e%2e%2f', '%2e%2e%5c'];
return traversalPatterns.some(pattern => originalPath.toLowerCase().includes(pattern));
}
isPathInWhitelist(canonicalPath) {
return this.config.allowedDirectories.some(allowedDir => {
const normalizedAllowed = path.resolve(allowedDir);
return canonicalPath === normalizedAllowed ||
canonicalPath.startsWith(normalizedAllowed + path.sep);
});
}
isExtensionAllowed(filePath) {
const ext = path.extname(filePath).toLowerCase();
return this.config.allowedExtensions.includes(ext);
}
async isSymbolicLink(filePath) {
try {
const stats = await fs.lstat(filePath);
return stats.isSymbolicLink();
}
catch {
return false;
}
}
logAuditEvent(type, originalPath, canonicalPath, violationType, context, validationTime, error) {
const auditEvent = {
id: `path_audit_${++this.auditCounter}_${Date.now()}`,
type,
originalPath,
canonicalPath,
violationType,
userAgent: context?.userAgent,
sessionId: context?.sessionId,
timestamp: new Date(),
validationTime: validationTime || 0,
stackTrace: error instanceof Error ? error.stack : undefined
};
this.auditEvents.push(auditEvent);
if (this.auditEvents.length > 1000) {
this.auditEvents = this.auditEvents.slice(-1000);
}
if (type === 'security_violation') {
logger.warn({
auditEvent,
originalPath,
canonicalPath,
violationType
}, 'Path security violation detected');
}
else if (type === 'validation_failure') {
logger.error({
auditEvent,
error: error instanceof Error ? error.message : String(error || 'Unknown error')
}, 'Path validation failed');
}
else {
logger.debug({
auditEvent
}, 'Path validation successful');
}
}
getAuditEvents(filter) {
let events = [...this.auditEvents];
if (filter) {
if (filter.type) {
events = events.filter(event => event.type === filter.type);
}
if (filter.violationType) {
events = events.filter(event => event.violationType === filter.violationType);
}
if (filter.since) {
events = events.filter(event => event.timestamp >= filter.since);
}
if (filter.sessionId) {
events = events.filter(event => event.sessionId === filter.sessionId);
}
}
return events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
getSecurityStatistics() {
const total = this.auditEvents.length;
const successful = this.auditEvents.filter(e => e.type === 'validation_success').length;
const violations = this.auditEvents.filter(e => e.type === 'security_violation').length;
const failures = this.auditEvents.filter(e => e.type === 'validation_failure').length;
const avgTime = total > 0
? this.auditEvents.reduce((sum, e) => sum + e.validationTime, 0) / total
: 0;
const violationsByType = {};
this.auditEvents
.filter(e => e.violationType)
.forEach(e => {
violationsByType[e.violationType] = (violationsByType[e.violationType] || 0) + 1;
});
return {
totalValidations: total,
successfulValidations: successful,
securityViolations: violations,
validationFailures: failures,
averageValidationTime: avgTime,
violationsByType
};
}
updateWhitelist(config) {
this.config = { ...this.config, ...config };
logger.info({ config: this.config }, 'Path whitelist configuration updated');
}
clearAuditEvents() {
this.auditEvents = [];
this.auditCounter = 0;
logger.info('Path security audit events cleared');
}
shutdown() {
this.auditEvents = [];
logger.info('Path Security Validator shutdown');
}
}
export async function validateSecurePath(filePath, operation = 'read', context) {
const validator = PathSecurityValidator.getInstance();
return validator.validatePath(filePath, operation, context);
}
export function getPathValidator() {
return PathSecurityValidator.getInstance();
}