mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
fix: address nitpick feedback from PR #423
## Security Fix (Command Injection) - Use `execFile` with argument arrays instead of string interpolation - Add `safeOpenInEditor` helper that properly handles `open -a` commands - Validate that worktreePath is an absolute path before execution - Prevents shell metacharacter injection attacks ## Shared Type Definition - Move `EditorInfo` interface to `@automaker/types` package - Server and UI now import from shared package to prevent drift - Re-export from use-available-editors.ts for convenience ## Remove Unused Code - Remove unused `defaultEditorName` prop from WorktreeActionsDropdown - Remove prop from WorktreeTab component interface - Remove useDefaultEditor hook usage from WorktreePanel - Export new hooks from hooks/index.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,18 +4,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from 'child_process';
|
import { exec, execFile } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
import { isAbsolute } from 'path';
|
||||||
|
import type { EditorInfo } from '@automaker/types';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
// Editor detection with caching
|
|
||||||
interface EditorInfo {
|
|
||||||
name: string;
|
|
||||||
command: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedEditor: EditorInfo | null = null;
|
let cachedEditor: EditorInfo | null = null;
|
||||||
let cachedEditors: EditorInfo[] | null = null;
|
let cachedEditors: EditorInfo[] | null = null;
|
||||||
@@ -177,6 +174,21 @@ export function createGetDefaultEditorHandler() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely execute an editor command with a path argument
|
||||||
|
* Uses execFile to prevent command injection
|
||||||
|
*/
|
||||||
|
async function safeOpenInEditor(command: string, targetPath: string): Promise<void> {
|
||||||
|
// Handle 'open -a "AppPath"' style commands (macOS)
|
||||||
|
if (command.startsWith('open -a ')) {
|
||||||
|
const appPath = command.replace('open -a ', '').replace(/"/g, '');
|
||||||
|
await execFileAsync('open', ['-a', appPath, targetPath]);
|
||||||
|
} else {
|
||||||
|
// Simple commands like 'code', 'cursor', 'zed', etc.
|
||||||
|
await execFileAsync(command, [targetPath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createOpenInEditorHandler() {
|
export function createOpenInEditorHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -193,6 +205,15 @@ export function createOpenInEditorHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Security: Validate that worktreePath is an absolute path
|
||||||
|
if (!isAbsolute(worktreePath)) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'worktreePath must be an absolute path',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use specified editor command or detect default
|
// Use specified editor command or detect default
|
||||||
let editor: EditorInfo;
|
let editor: EditorInfo;
|
||||||
if (editorCommand) {
|
if (editorCommand) {
|
||||||
@@ -205,7 +226,7 @@ export function createOpenInEditorHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsync(`${editor.command} "${worktreePath}"`);
|
await safeOpenInEditor(editor.command, worktreePath);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
@@ -216,21 +237,21 @@ export function createOpenInEditorHandler() {
|
|||||||
} catch (editorError) {
|
} catch (editorError) {
|
||||||
// If the detected editor fails, try opening in default file manager as fallback
|
// If the detected editor fails, try opening in default file manager as fallback
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
let openCommand: string;
|
let fallbackCommand: string;
|
||||||
let fallbackName: string;
|
let fallbackName: string;
|
||||||
|
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
openCommand = `open "${worktreePath}"`;
|
fallbackCommand = 'open';
|
||||||
fallbackName = 'Finder';
|
fallbackName = 'Finder';
|
||||||
} else if (platform === 'win32') {
|
} else if (platform === 'win32') {
|
||||||
openCommand = `explorer "${worktreePath}"`;
|
fallbackCommand = 'explorer';
|
||||||
fallbackName = 'Explorer';
|
fallbackName = 'Explorer';
|
||||||
} else {
|
} else {
|
||||||
openCommand = `xdg-open "${worktreePath}"`;
|
fallbackCommand = 'xdg-open';
|
||||||
fallbackName = 'File Manager';
|
fallbackName = 'File Manager';
|
||||||
}
|
}
|
||||||
|
|
||||||
await execAsync(openCommand);
|
await execFileAsync(fallbackCommand, [worktreePath]);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import { getEditorIcon } from '@/components/icons/editor-icons';
|
|||||||
interface WorktreeActionsDropdownProps {
|
interface WorktreeActionsDropdownProps {
|
||||||
worktree: WorktreeInfo;
|
worktree: WorktreeInfo;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
defaultEditorName: string;
|
|
||||||
aheadCount: number;
|
aheadCount: number;
|
||||||
behindCount: number;
|
behindCount: number;
|
||||||
isPulling: boolean;
|
isPulling: boolean;
|
||||||
@@ -60,7 +59,6 @@ interface WorktreeActionsDropdownProps {
|
|||||||
export function WorktreeActionsDropdown({
|
export function WorktreeActionsDropdown({
|
||||||
worktree,
|
worktree,
|
||||||
isSelected,
|
isSelected,
|
||||||
defaultEditorName,
|
|
||||||
aheadCount,
|
aheadCount,
|
||||||
behindCount,
|
behindCount,
|
||||||
isPulling,
|
isPulling,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ interface WorktreeTabProps {
|
|||||||
isActivating: boolean;
|
isActivating: boolean;
|
||||||
isDevServerRunning: boolean;
|
isDevServerRunning: boolean;
|
||||||
devServerInfo?: DevServerInfo;
|
devServerInfo?: DevServerInfo;
|
||||||
defaultEditorName: string;
|
|
||||||
branches: BranchInfo[];
|
branches: BranchInfo[];
|
||||||
filteredBranches: BranchInfo[];
|
filteredBranches: BranchInfo[];
|
||||||
branchFilter: string;
|
branchFilter: string;
|
||||||
@@ -58,7 +57,6 @@ export function WorktreeTab({
|
|||||||
isActivating,
|
isActivating,
|
||||||
isDevServerRunning,
|
isDevServerRunning,
|
||||||
devServerInfo,
|
devServerInfo,
|
||||||
defaultEditorName,
|
|
||||||
branches,
|
branches,
|
||||||
filteredBranches,
|
filteredBranches,
|
||||||
branchFilter,
|
branchFilter,
|
||||||
@@ -315,7 +313,6 @@ export function WorktreeTab({
|
|||||||
<WorktreeActionsDropdown
|
<WorktreeActionsDropdown
|
||||||
worktree={worktree}
|
worktree={worktree}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
defaultEditorName={defaultEditorName}
|
|
||||||
aheadCount={aheadCount}
|
aheadCount={aheadCount}
|
||||||
behindCount={behindCount}
|
behindCount={behindCount}
|
||||||
isPulling={isPulling}
|
isPulling={isPulling}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ export { useWorktrees } from './use-worktrees';
|
|||||||
export { useDevServers } from './use-dev-servers';
|
export { useDevServers } from './use-dev-servers';
|
||||||
export { useBranches } from './use-branches';
|
export { useBranches } from './use-branches';
|
||||||
export { useWorktreeActions } from './use-worktree-actions';
|
export { useWorktreeActions } from './use-worktree-actions';
|
||||||
export { useDefaultEditor } from './use-default-editor';
|
|
||||||
export { useRunningFeatures } from './use-running-features';
|
export { useRunningFeatures } from './use-running-features';
|
||||||
|
export { useAvailableEditors, useEffectiveDefaultEditor } from './use-available-editors';
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
|||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import type { EditorInfo } from '@automaker/types';
|
||||||
|
|
||||||
const logger = createLogger('AvailableEditors');
|
const logger = createLogger('AvailableEditors');
|
||||||
|
|
||||||
export interface EditorInfo {
|
// Re-export EditorInfo for convenience
|
||||||
name: string;
|
export type { EditorInfo };
|
||||||
command: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAvailableEditors() {
|
export function useAvailableEditors() {
|
||||||
const [editors, setEditors] = useState<EditorInfo[]>([]);
|
const [editors, setEditors] = useState<EditorInfo[]>([]);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
useDevServers,
|
useDevServers,
|
||||||
useBranches,
|
useBranches,
|
||||||
useWorktreeActions,
|
useWorktreeActions,
|
||||||
useDefaultEditor,
|
|
||||||
useRunningFeatures,
|
useRunningFeatures,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
import { WorktreeTab } from './components';
|
import { WorktreeTab } from './components';
|
||||||
@@ -76,8 +75,6 @@ export function WorktreePanel({
|
|||||||
fetchBranches,
|
fetchBranches,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { defaultEditorName } = useDefaultEditor();
|
|
||||||
|
|
||||||
const { hasRunningFeatures } = useRunningFeatures({
|
const { hasRunningFeatures } = useRunningFeatures({
|
||||||
runningFeatureIds,
|
runningFeatureIds,
|
||||||
features,
|
features,
|
||||||
@@ -192,7 +189,6 @@ export function WorktreePanel({
|
|||||||
isActivating={isActivating}
|
isActivating={isActivating}
|
||||||
isDevServerRunning={isDevServerRunning(mainWorktree)}
|
isDevServerRunning={isDevServerRunning(mainWorktree)}
|
||||||
devServerInfo={getDevServerInfo(mainWorktree)}
|
devServerInfo={getDevServerInfo(mainWorktree)}
|
||||||
defaultEditorName={defaultEditorName}
|
|
||||||
branches={branches}
|
branches={branches}
|
||||||
filteredBranches={filteredBranches}
|
filteredBranches={filteredBranches}
|
||||||
branchFilter={branchFilter}
|
branchFilter={branchFilter}
|
||||||
@@ -247,7 +243,6 @@ export function WorktreePanel({
|
|||||||
isActivating={isActivating}
|
isActivating={isActivating}
|
||||||
isDevServerRunning={isDevServerRunning(worktree)}
|
isDevServerRunning={isDevServerRunning(worktree)}
|
||||||
devServerInfo={getDevServerInfo(worktree)}
|
devServerInfo={getDevServerInfo(worktree)}
|
||||||
defaultEditorName={defaultEditorName}
|
|
||||||
branches={branches}
|
branches={branches}
|
||||||
filteredBranches={filteredBranches}
|
filteredBranches={filteredBranches}
|
||||||
branchFilter={branchFilter}
|
branchFilter={branchFilter}
|
||||||
|
|||||||
13
libs/types/src/editor.ts
Normal file
13
libs/types/src/editor.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Editor types for the "Open In" functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about an available code editor
|
||||||
|
*/
|
||||||
|
export interface EditorInfo {
|
||||||
|
/** Display name of the editor (e.g., "VS Code", "Cursor") */
|
||||||
|
name: string;
|
||||||
|
/** CLI command or open command to launch the editor */
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
@@ -204,6 +204,9 @@ export type {
|
|||||||
// Port configuration
|
// Port configuration
|
||||||
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './ports.js';
|
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './ports.js';
|
||||||
|
|
||||||
|
// Editor types
|
||||||
|
export type { EditorInfo } from './editor.js';
|
||||||
|
|
||||||
// Ideation types
|
// Ideation types
|
||||||
export type {
|
export type {
|
||||||
IdeaCategory,
|
IdeaCategory,
|
||||||
|
|||||||
Reference in New Issue
Block a user