packfs-core
Version:
Semantic filesystem operations for LLM agent frameworks with natural language understanding. See LLM_AGENT_GUIDE.md for copy-paste examples.
180 lines • 6.33 kB
JavaScript
/**
* Security validator for Mastra filesystem operations
*/
import { resolve, normalize, extname, isAbsolute } from 'path';
import { DEFAULT_SECURITY_CONFIG } from './config.js';
/**
* Security validator that enforces path restrictions, file limits, and rate limiting
*/
export class MastraSecurityValidator {
constructor(config) {
this.requestCounts = new Map();
// Merge with defaults
this.config = {
rootPath: config.rootPath,
maxFileSize: config.maxFileSize ?? DEFAULT_SECURITY_CONFIG.maxFileSize,
// Only apply default extensions if user didn't explicitly set allowedExtensions to undefined
allowedExtensions: config.allowedExtensions !== undefined ? config.allowedExtensions : undefined,
blockedPaths: config.blockedPaths ?? DEFAULT_SECURITY_CONFIG.blockedPaths,
rateLimiting: config.rateLimiting ?? DEFAULT_SECURITY_CONFIG.rateLimiting
};
// Validate root path
if (!isAbsolute(this.config.rootPath)) {
throw new Error('Root path must be absolute');
}
// Normalize root path
this.config.rootPath = normalize(this.config.rootPath);
}
/**
* Validate a file path for security compliance
*/
validatePath(path) {
try {
// Normalize the path
const normalizedPath = this.normalizePath(path);
// Check if path is within root
if (!this.isWithinRoot(normalizedPath)) {
return {
valid: false,
reason: 'Path outside allowed root directory'
};
}
// Check for blocked path segments
const blockedSegment = this.getBlockedSegment(normalizedPath);
if (blockedSegment) {
return {
valid: false,
reason: `Path contains blocked segment: ${blockedSegment}`
};
}
// Validate file extension if it appears to be a file and extensions are configured
if (this.isFilePath(normalizedPath) && this.config.allowedExtensions && this.config.allowedExtensions.length > 0) {
const ext = extname(normalizedPath);
if (ext && !this.config.allowedExtensions.includes(ext)) {
return {
valid: false,
reason: `File extension not allowed: ${ext}`
};
}
}
return { valid: true };
}
catch (error) {
return {
valid: false,
reason: `Path validation error: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Validate an intent operation for security compliance
*/
validateOperation(intent) {
// Validate the target path
const pathValidation = this.validatePath(intent.target.path);
if (!pathValidation.valid) {
return pathValidation;
}
// Check rate limiting if configured
if (this.config.rateLimiting) {
const rateLimitCheck = this.checkRateLimit(intent.target.path);
if (!rateLimitCheck.valid) {
return rateLimitCheck;
}
}
// Validate file size limits for read operations
if ('preferences' in intent && intent.preferences?.maxSize) {
if (intent.preferences.maxSize > this.config.maxFileSize) {
return {
valid: false,
reason: `Requested size exceeds maximum allowed: ${this.config.maxFileSize} bytes`
};
}
}
// Additional validation for update operations
if (intent.purpose === 'delete') {
// Could add additional checks for delete operations
// For now, path validation is sufficient
}
return { valid: true };
}
/**
* Check rate limiting for a specific path
*/
checkRateLimit(path) {
if (!this.config.rateLimiting) {
return { valid: true };
}
const now = Date.now();
const key = path;
const limits = this.config.rateLimiting;
let record = this.requestCounts.get(key);
// Reset or initialize record if needed
if (!record || now > record.resetTime) {
record = {
count: 1,
resetTime: now + limits.windowMs
};
this.requestCounts.set(key, record);
return { valid: true };
}
// Increment count
record.count++;
// Check if limit exceeded
if (record.count > limits.maxRequests) {
return {
valid: false,
reason: `Rate limit exceeded: ${record.count}/${limits.maxRequests} requests in ${limits.windowMs}ms window`
};
}
return { valid: true };
}
/**
* Normalize a path for consistent validation
*/
normalizePath(path) {
// Handle absolute paths by making them relative to root
if (isAbsolute(path)) {
return normalize(path);
}
// Resolve relative paths against root
return resolve(this.config.rootPath, path);
}
/**
* Check if a path is within the allowed root directory
*/
isWithinRoot(normalizedPath) {
return normalizedPath.startsWith(this.config.rootPath);
}
/**
* Check if path contains any blocked segments
*/
getBlockedSegment(normalizedPath) {
for (const blocked of this.config.blockedPaths) {
if (normalizedPath.includes(blocked)) {
return blocked;
}
}
return null;
}
/**
* Determine if a path appears to be a file (has extension)
*/
isFilePath(path) {
const ext = extname(path);
return ext.length > 0;
}
/**
* Get current configuration (for debugging/testing)
*/
getConfig() {
return { ...this.config };
}
/**
* Clear rate limit records (for testing)
*/
clearRateLimits() {
this.requestCounts.clear();
}
}
//# sourceMappingURL=validator.js.map