refactor: integrate secure file system operations across services

This commit replaces direct file system operations with a secure file system adapter to enhance security by enforcing path validation. The changes include:

- Replaced `fs` imports with `secureFs` in various services and utilities.
- Updated file operations in `agent-service`, `auto-mode-service`, `feature-loader`, and `settings-service` to use the secure file system methods.
- Ensured that all file I/O operations are validated against the ALLOWED_ROOT_DIRECTORY.

This refactor aims to prevent unauthorized file access and improve overall security posture.

Tests: All unit tests passing.

🤖 Generated with Claude Code
This commit is contained in:
Test User
2025-12-20 18:45:39 -05:00
parent ade80484bb
commit f3c9e828e2
45 changed files with 329 additions and 551 deletions

View File

@@ -9,7 +9,7 @@
* Directory creation is handled separately by ensure* functions.
*/
import fs from "fs/promises";
import * as secureFs from "./secure-fs.js";
import path from "path";
/**
@@ -149,7 +149,7 @@ export function getBranchTrackingPath(projectPath: string): string {
*/
export async function ensureAutomakerDir(projectPath: string): Promise<string> {
const automakerDir = getAutomakerDir(projectPath);
await fs.mkdir(automakerDir, { recursive: true });
await secureFs.mkdir(automakerDir, { recursive: true });
return automakerDir;
}
@@ -211,6 +211,6 @@ export function getProjectSettingsPath(projectPath: string): string {
* @returns Promise resolving to the created data directory path
*/
export async function ensureDataDir(dataDir: string): Promise<string> {
await fs.mkdir(dataDir, { recursive: true });
await secureFs.mkdir(dataDir, { recursive: true });
return dataDir;
}

View File

@@ -2,7 +2,7 @@
* File system utilities that handle symlinks safely
*/
import fs from "fs/promises";
import * as secureFs from "./secure-fs.js";
import path from "path";
/**
@@ -14,7 +14,7 @@ export async function mkdirSafe(dirPath: string): Promise<void> {
// Check if path already exists using lstat (doesn't follow symlinks)
try {
const stats = await fs.lstat(resolvedPath);
const stats = await secureFs.lstat(resolvedPath);
// Path exists - if it's a directory or symlink, consider it success
if (stats.isDirectory() || stats.isSymbolicLink()) {
return;
@@ -36,7 +36,7 @@ export async function mkdirSafe(dirPath: string): Promise<void> {
// Path doesn't exist, create it
try {
await fs.mkdir(resolvedPath, { recursive: true });
await secureFs.mkdir(resolvedPath, { recursive: true });
} catch (error: any) {
// Handle race conditions and symlink issues
if (error.code === "EEXIST" || error.code === "ELOOP") {
@@ -52,7 +52,7 @@ export async function mkdirSafe(dirPath: string): Promise<void> {
*/
export async function existsSafe(filePath: string): Promise<boolean> {
try {
await fs.lstat(filePath);
await secureFs.lstat(filePath);
return true;
} catch (error: any) {
if (error.code === "ENOENT") {

View File

@@ -8,7 +8,7 @@
* - Path resolution (relative/absolute)
*/
import fs from "fs/promises";
import * as secureFs from "./secure-fs.js";
import path from "path";
/**
@@ -63,7 +63,7 @@ export function getMimeTypeForImage(imagePath: string): string {
* @throws Error if file cannot be read
*/
export async function readImageAsBase64(imagePath: string): Promise<ImageData> {
const imageBuffer = await fs.readFile(imagePath);
const imageBuffer = await secureFs.readFile(imagePath) as Buffer;
const base64Data = imageBuffer.toString("base64");
const mimeType = getMimeTypeForImage(imagePath);

View File

@@ -0,0 +1,156 @@
/**
* Secure File System Adapter
*
* All file I/O operations must go through this adapter to enforce
* ALLOWED_ROOT_DIRECTORY restrictions at the actual access point,
* not just at the API layer. This provides defense-in-depth security.
*/
import fs from "fs/promises";
import path from "path";
import { validatePath } from "./security.js";
/**
* Wrapper around fs.access that validates path first
*/
export async function access(filePath: string, mode?: number): Promise<void> {
const validatedPath = validatePath(filePath);
return fs.access(validatedPath, mode);
}
/**
* Wrapper around fs.readFile that validates path first
*/
export async function readFile(
filePath: string,
encoding?: BufferEncoding
): Promise<string | Buffer> {
const validatedPath = validatePath(filePath);
if (encoding) {
return fs.readFile(validatedPath, encoding);
}
return fs.readFile(validatedPath);
}
/**
* Wrapper around fs.writeFile that validates path first
*/
export async function writeFile(
filePath: string,
data: string | Buffer,
encoding?: BufferEncoding
): Promise<void> {
const validatedPath = validatePath(filePath);
return fs.writeFile(validatedPath, data, encoding as any);
}
/**
* Wrapper around fs.mkdir that validates path first
*/
export async function mkdir(
dirPath: string,
options?: { recursive?: boolean; mode?: number }
): Promise<string | undefined> {
const validatedPath = validatePath(dirPath);
return fs.mkdir(validatedPath, options);
}
/**
* Wrapper around fs.readdir that validates path first
*/
export async function readdir(
dirPath: string,
options?: { withFileTypes?: boolean; encoding?: BufferEncoding }
): Promise<string[] | any[]> {
const validatedPath = validatePath(dirPath);
return fs.readdir(validatedPath, options as any);
}
/**
* Wrapper around fs.stat that validates path first
*/
export async function stat(filePath: string): Promise<any> {
const validatedPath = validatePath(filePath);
return fs.stat(validatedPath);
}
/**
* Wrapper around fs.rm that validates path first
*/
export async function rm(
filePath: string,
options?: { recursive?: boolean; force?: boolean }
): Promise<void> {
const validatedPath = validatePath(filePath);
return fs.rm(validatedPath, options);
}
/**
* Wrapper around fs.unlink that validates path first
*/
export async function unlink(filePath: string): Promise<void> {
const validatedPath = validatePath(filePath);
return fs.unlink(validatedPath);
}
/**
* Wrapper around fs.copyFile that validates both paths first
*/
export async function copyFile(
src: string,
dest: string,
mode?: number
): Promise<void> {
const validatedSrc = validatePath(src);
const validatedDest = validatePath(dest);
return fs.copyFile(validatedSrc, validatedDest, mode);
}
/**
* Wrapper around fs.appendFile that validates path first
*/
export async function appendFile(
filePath: string,
data: string | Buffer,
encoding?: BufferEncoding
): Promise<void> {
const validatedPath = validatePath(filePath);
return fs.appendFile(validatedPath, data, encoding as any);
}
/**
* Wrapper around fs.rename that validates both paths first
*/
export async function rename(
oldPath: string,
newPath: string
): Promise<void> {
const validatedOldPath = validatePath(oldPath);
const validatedNewPath = validatePath(newPath);
return fs.rename(validatedOldPath, validatedNewPath);
}
/**
* Wrapper around fs.lstat that validates path first
* Returns file stats without following symbolic links
*/
export async function lstat(filePath: string): Promise<any> {
const validatedPath = validatePath(filePath);
return fs.lstat(validatedPath);
}
/**
* Wrapper around path.join that returns resolved path
* Does NOT validate - use this for path construction, then pass to other operations
*/
export function joinPath(...pathSegments: string[]): string {
return path.join(...pathSegments);
}
/**
* Wrapper around path.resolve that returns resolved path
* Does NOT validate - use this for path construction, then pass to other operations
*/
export function resolvePath(...pathSegments: string[]): string {
return path.resolve(...pathSegments);
}