From 0c0a00a61860b6f3a6e19877bd8188b1844d9de2 Mon Sep 17 00:00:00 2001 From: Coden Date: Fri, 12 Jun 2026 17:01:20 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(claude):=20git=20branch=EB=AA=85=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20cap=EC=9C=BC=EB=A1=9C=20=EC=86=90=EC=83=81?= =?UTF-8?q?/=EC=A1=B0=EC=9E=91=EB=90=9C=20.git/HEAD=20=EB=B0=A9=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read_branch_from_git_dir에 MAX_BRANCH_LEN(256B) 상한을 추가한다. loose ref는 파일시스템 경로 컴포넌트라 정상 branch명은 APFS NAME_MAX(255)를 넘을 수 없어 256B를 절대 초과하지 않는다. 따라서 256B 초과는 손상/조작 신호이므로 표시를 거부해 false-positive(틀린 branch)보다 false-negative(빈 pill)를 택한다(FP --- src/claude.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/claude.rs b/src/claude.rs index 51c55fc..1d17698 100644 --- a/src/claude.rs +++ b/src/claude.rs @@ -364,19 +364,35 @@ fn is_safe_base_path(base_path: &str) -> bool { .any(|component| matches!(component, Component::ParentDir)) } +/// branch명 byte length 상한(방어적 상한, FP`)로 저장되므로 정상 branch명은 +/// APFS NAME_MAX(255)를 넘을 수 없어 256B를 절대 초과하지 않는다. 따라서 256B 초과는 정상 git이 +/// 만들 수 없는 손상/조작된 `.git/HEAD` 신호이며, 표시를 거부해 false-positive(틀린 branch)보다 +/// false-negative(빈 pill)를 택하는 방어적 상한이다(FP` 형식의 HEAD에서 추출한 ``. detached HEAD(직접 SHA)나 -/// 읽기 실패 시 `None`. 부재/실패에 안전(절대 패닉하지 않음). +/// `ref: refs/heads/` 형식의 HEAD에서 추출한 ``. 다음 4원인 중 하나라도 해당하면 +/// `None`을 반환한다(부재/실패에 안전 — 절대 패닉하지 않음): +/// 1. `.git/HEAD` 부재 또는 읽기 실패(canonicalize/read 실패). +/// 2. detached HEAD(`ref:` 접두 없이 SHA 직접 기록). +/// 3. 외부향 심볼릭 HEAD(canonicalize 결과가 `.git/HEAD`로 끝나지 않음 — 누출 방어 위반). +/// 4. branch명이 비었거나 제어문자를 포함하거나 [`MAX_BRANCH_LEN`]을 초과. /// /// # 주의 -/// branch명은 제어문자 미포함만 허용한다(터미널/status 인젝션 방어). 신뢰 불가 `.git/HEAD`가 -/// ESC/개행/CR 등 제어문자가 섞인 branch명을 담으면 oneline SGR/cmux pill로 그대로 렌더돼 -/// 인젝션이 되므로, 정상 git branch명이 절대 갖지 않는 제어문자를 source chokepoint에서 거부한다. +/// - branch명은 제어문자 미포함만 허용한다(터미널/status 인젝션 방어). 신뢰 불가 `.git/HEAD`가 +/// ESC/개행/CR 등 제어문자가 섞인 branch명을 담으면 oneline SGR/cmux pill로 그대로 렌더돼 +/// 인젝션이 되므로, 정상 git branch명이 절대 갖지 않는 제어문자를 source chokepoint에서 거부한다. +/// - 심볼릭 `.git` 추종은 의도된 표준 git 동작 — canonicalize 가드는 결과가 `.git/HEAD`로 끝나는지만 +/// 확인(외부향 누출 차단), 추종 자체는 허용한다. +/// - 동기 fs read는 의도적 — `/.git/HEAD` 단일 소파일을 1회 read한다. 느린 네트워크 마운트 +/// (NFS 등)에서 status 렌더가 블록될 수 있다(알려진 트레이드오프). 완화(timeout/캐시)는 future work. fn read_branch_from_git_dir(base_path: &str) -> Option { use std::path::Path; // 표준 워크트리는 `/.git/HEAD`. (linked worktree의 gitfile 케이스는 v1 범위 밖.) @@ -395,7 +411,8 @@ fn read_branch_from_git_dir(base_path: &str) -> Option { let branch = trimmed.strip_prefix("ref: refs/heads/")?; // 인젝션 방어: 신뢰 불가 HEAD 내용에 제어문자(ESC/개행/CR/기타 C0·DEL)가 섞이면 거부한다. // 정상 git branch명은 제어문자를 절대 갖지 않으므로 정상 케이스 회귀는 0이다. - if branch.is_empty() || branch.chars().any(char::is_control) { + // SECURITY: 256B 초과 branch명 = 손상/조작된 .git/HEAD 신호 → 표시 거부(FP MAX_BRANCH_LEN { None } else { Some(branch.to_string()) From da1a6c08a7227af3599d3708123d116c3ec76e29 Mon Sep 17 00:00:00 2001 From: Coden Date: Fri, 12 Jun 2026 17:01:53 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test(claude):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C=20=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?RAII=20=EA=B0=80=EB=93=9C=EB=A1=9C=20=ED=8C=A8=EB=8B=89=20?= =?UTF-8?q?=EB=88=84=EC=88=98=200=20+=20=EC=8B=A0=EA=B7=9C=20=ED=9A=8C?= =?UTF-8?q?=EA=B7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit unique_test_dir를 TestDir 가드 struct 반환형으로 승격한다. Drop에서 remove_dir_all로 정리해 패닉(단언 실패) 시에도 temp 디렉터리 누수가 0이다. Deref로 기존 호출부의 .join()/.to_string_lossy()는 무변경 동작한다. (주의: unwind 전제 — panic="abort" 도입 시 무효, 현재 Cargo.toml은 unwind 기본.) - 구식 PID-only temp 경로 3곳(git-test/repoobj/detached)을 unique_test_dir로 통일해 collision + panic-leak을 동시 해소한다. - 말미 remove_dir_all 정리 라인은 전부 제거(이제 Drop이 처리). - test_dir_guard_cleans_up_on_panic: catch_unwind로 의도적 panic을 감싼 뒤 캡처한 경로 부재를 단언해 가드가 언와인딩에서도 cleanup함을 증명(AC-d1). - derive_from_cwd_rejects_overlong_branch: 256B 초과 거부 + 정확히 256B 허용 경계값을 고정(item a 회귀 가드). - derive_from_cwd_symlink_git_dir_follows_to_some: 심볼릭 .git이 정상 ref를 가리키면 Some(branch)를 반환해 표준 git symlink 추종이 의도 동작임을 고정. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/claude.rs | 152 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 126 insertions(+), 26 deletions(-) diff --git a/src/claude.rs b/src/claude.rs index 1d17698..796e2ae 100644 --- a/src/claude.rs +++ b/src/claude.rs @@ -637,23 +637,81 @@ struct RawLtermInput { mod tests { use super::*; - /// 호출마다 고유한 비존재 절대 temp 경로를 반환한다(테스트 격리). + /// 테스트용 고유 임시 디렉터리 경로 + RAII 정리 가드. + /// Drop에서 remove_dir_all로 정리해 패닉(단언 실패) 시에도 누수 0. + /// 주의: unwind 전제 — `[profile.test] panic="abort"` 도입 시 Drop 미실행으로 무효(현재 Cargo.toml은 unwind 기본). + struct TestDir(std::path::PathBuf); + impl Drop for TestDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + impl std::ops::Deref for TestDir { + type Target = std::path::Path; + fn deref(&self) -> &std::path::Path { + &self.0 + } + } + + /// 호출마다 고유한 비존재 절대 temp 경로를 RAII 가드로 감싸 반환한다(테스트 격리). /// /// # 인자 /// - `label`: 경로를 사람이 식별하기 위한 라벨(테스트 의도 표시). /// /// # 반환 - /// `/understatus-