chore: bump version to 0.1531

This commit is contained in:
arkon
2026-02-18 10:23:10 +01:00
parent e05811d69a
commit 62ddfdea36
9 changed files with 298 additions and 45 deletions
+31 -9
View File
@@ -12,7 +12,7 @@
*/
import { spawn, ChildProcess } from 'node:child_process';
import { existsSync, statSync } from 'node:fs';
import { existsSync, statSync, realpathSync } from 'node:fs';
import { resolve, relative, isAbsolute } from 'node:path';
import { homedir } from 'node:os';
import { EventEmitter } from 'node:events';
@@ -158,7 +158,7 @@ export class FileStreamManager extends EventEmitter {
return { success: false, error: validationResult.error };
}
const absolutePath = validationResult.absolutePath!;
let absolutePath = validationResult.absolutePath!;
// Check file exists and size
try {
@@ -175,6 +175,22 @@ export class FileStreamManager extends EventEmitter {
return { success: false, error: 'File not found or not accessible' };
}
// Re-resolve symlinks right before spawn to minimize TOCTOU window.
// A symlink could have been swapped between validatePath() and here.
try {
const resolvedPath = realpathSync(absolutePath);
if (resolvedPath !== absolutePath) {
// Symlink target changed — re-validate against allowed paths
const recheck = this.validatePath(resolvedPath, workingDir);
if (!recheck.valid) {
return { success: false, error: recheck.error };
}
absolutePath = resolvedPath;
}
} catch {
return { success: false, error: 'File not found or not accessible' };
}
// Generate stream ID
const streamId = `${sessionId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -363,19 +379,27 @@ export class FileStreamManager extends EventEmitter {
}
// Resolve to absolute path
const absolutePath = isAbsolute(expandedPath)
let absolutePath = isAbsolute(expandedPath)
? resolve(expandedPath)
: resolve(workingDir, expandedPath);
// Resolve symlinks to prevent symlink attacks — validate the real target,
// not the symlink itself. Fall back to resolved path if file doesn't exist yet.
try {
absolutePath = realpathSync(absolutePath);
} catch {
// File may not exist yet (tail -f can wait); keep the resolved path
// which will be caught by the existsSync check below
}
// Normalize the working directory
const normalizedWorkingDir = resolve(workingDir);
// Check if the resolved path is within the working directory
// or common log directories
// or common log directories (/tmp intentionally excluded — world-writable)
const allowedPaths = [
normalizedWorkingDir,
'/var/log',
'/tmp',
resolve(homedir(), '.local/share'),
resolve(homedir(), '.cache'),
resolve(homedir(), 'logs'),
@@ -393,10 +417,8 @@ export class FileStreamManager extends EventEmitter {
};
}
// Check for path traversal attempts
if (absolutePath.includes('..')) {
return { valid: false, error: 'Path traversal not allowed' };
}
// Note: No need to check for '..' — resolve() already normalizes the path,
// and realpathSync() resolves symlinks. Both eliminate traversal sequences.
// Check file exists
if (!existsSync(absolutePath)) {