fix: harden utils.js edge cases and add input validation

- Guard findFiles() against null/undefined dir and pattern parameters
  (previously crashed with TypeError on .replace() or fs.existsSync())
- Wrap countInFile() and grepFile() regex construction in try-catch to
  handle invalid regex strings like '(unclosed' (previously crashed with
  SyntaxError: Invalid regular expression)
- Add try-catch to replaceInFile() with descriptive error logging
- Add 1MB size limit to readStdinJson() matching the PostToolUse hooks
  (previously had unbounded stdin accumulation)
- Improve ensureDir() error message to include the directory path
- Add 128-char length limit to setAlias() to prevent oversized alias
  names from inflating the JSON store
- Update utils.d.ts with new maxSize option on ReadStdinJsonOptions
This commit is contained in:
Affaan Mustafa
2026-02-12 14:49:11 -08:00
parent 6686cb9bda
commit 9e791ed305
3 changed files with 42 additions and 12 deletions

View File

@@ -194,6 +194,10 @@ function setAlias(alias, sessionPath, title = null) {
return { success: false, error: 'Session path cannot be empty' };
}
if (alias.length > 128) {
return { success: false, error: 'Alias name cannot exceed 128 characters' };
}
if (!/^[a-zA-Z0-9_-]+$/.test(alias)) {
return { success: false, error: 'Alias name must contain only letters, numbers, dashes, and underscores' };
}

View File

@@ -123,6 +123,11 @@ export interface ReadStdinJsonOptions {
* if stdin never closes. Default: 5000
*/
timeoutMs?: number;
/**
* Maximum stdin data size in bytes. Prevents unbounded memory growth.
* Default: 1048576 (1MB)
*/
maxSize?: number;
}
/**

View File

@@ -62,7 +62,7 @@ function ensureDir(dirPath) {
} catch (err) {
// EEXIST is fine (race condition with another process creating it)
if (err.code !== 'EEXIST') {
throw err;
throw new Error(`Failed to create directory '${dirPath}': ${err.message}`);
}
}
return dirPath;
@@ -140,6 +140,9 @@ function getDateTimeString() {
* @param {object} options - Options { maxAge: days, recursive: boolean }
*/
function findFiles(dir, pattern, options = {}) {
if (!dir || typeof dir !== 'string') return [];
if (!pattern || typeof pattern !== 'string') return [];
const { maxAge = null, recursive = false } = options;
const results = [];
@@ -201,7 +204,7 @@ function findFiles(dir, pattern, options = {}) {
* @returns {Promise<object>} Parsed JSON object, or empty object if stdin is empty
*/
async function readStdinJson(options = {}) {
const { timeoutMs = 5000 } = options;
const { timeoutMs = 5000, maxSize = 1024 * 1024 } = options;
return new Promise((resolve, reject) => {
let data = '';
@@ -221,7 +224,9 @@ async function readStdinJson(options = {}) {
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
data += chunk;
if (data.length < maxSize) {
data += chunk;
}
});
process.stdin.on('end', () => {
@@ -382,9 +387,14 @@ function replaceInFile(filePath, search, replace) {
const content = readFile(filePath);
if (content === null) return false;
const newContent = content.replace(search, replace);
writeFile(filePath, newContent);
return true;
try {
const newContent = content.replace(search, replace);
writeFile(filePath, newContent);
return true;
} catch (err) {
log(`[Utils] replaceInFile failed for ${filePath}: ${err.message}`);
return false;
}
}
/**
@@ -400,11 +410,17 @@ function countInFile(filePath, pattern) {
if (content === null) return 0;
let regex;
if (pattern instanceof RegExp) {
// Ensure global flag is set for correct counting
regex = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags + 'g');
} else {
regex = new RegExp(pattern, 'g');
try {
if (pattern instanceof RegExp) {
// Ensure global flag is set for correct counting
regex = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags + 'g');
} else if (typeof pattern === 'string') {
regex = new RegExp(pattern, 'g');
} else {
return 0;
}
} catch {
return 0; // Invalid regex pattern
}
const matches = content.match(regex);
return matches ? matches.length : 0;
@@ -417,7 +433,12 @@ function grepFile(filePath, pattern) {
const content = readFile(filePath);
if (content === null) return [];
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
let regex;
try {
regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
} catch {
return []; // Invalid regex pattern
}
const lines = content.split('\n');
const results = [];