Skip to content

fix(mcp): 조회 전용 프로필로 원본 문서를 덮어쓸 수 있다 — 세션 축 권한 경계 이탈 (#3629 후속) - #3723

Closed
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:pr/mcp-profile-session-boundary
Closed

fix(mcp): 조회 전용 프로필로 원본 문서를 덮어쓸 수 있다 — 세션 축 권한 경계 이탈 (#3629 후속)#3723
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:pr/mcp-profile-session-boundary

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

요약

mcp-serve --profile 아카이브검색(대량 RAG·감사용 조회 전용 직무, 허용 도구 7종에 쓰기 도구가 하나도 없습니다)으로 원본 문서를 덮어쓸 수 있습니다. 실측:

$ # --profile 아카이브검색 로 mcp-serve 구동
before: 31232 bytes
tools/list = 19 tools
  노출된 쓰기 도구: ['hwp_doc_replace_text', 'hwp_doc_set_cell', 'hwp_doc_fill_fields', 'hwp_doc_save']

id=3 hwp_open      isError=False -> {"docId":"doc-1","pageCount":6,...}
id=4 hwp_doc_save  isError=False -> {"bytes":18432,"output":"...\readonly_profile_victim.hwp",...}

after: 18432 bytes  -> OVERWRITTEN

동시에 선언과 서버가 어긋납니다capabilities --mcp --profile 아카이브검색7종을 선언하는데 서버는 19종을 냅니다.

원인

프로필의 세션 축이 bool 하나였습니다.

pub session: bool,   // 세션 도구 12종 전부 or 전무

served_tools 는 그 불리언이 참이면 12종을 무조건 push 하고, 호출 게이트도 !include_session && is_session_tool(name) 이라 같은 구멍을 그대로 물려받습니다. 그래서 "조회는 세션으로 반복하되 쓰기는 안 함"이라는 직무를 표현할 방법 자체가 없었고, 그 직무가 세션을 켜는 순간 편집·저장 4종이 딸려 왔습니다.

mcp_serve.rs 의 주석이 정확히 이 계약을 선언하고 있습니다:

"tools/list에서 제거한 세션 도구는 호출로 우회할 수도 없어야 한다. 프로필은 추천 목록이 아니라 서버가 실제로 제공하는 도구 집합의 경계다."

선언된 경계(7종)와 강제되는 경계(19종)가 달랐습니다.

가드가 놓친 이유: tests/agent_profile_router_contract.rs행정서식(세션+편집 의도)과 데이터분석(세션 off) 두 가지만 시험합니다 — 조회 전용인데 세션은 켜는 조합이 미시험이었습니다.

수정

pub session_tools: Option<&'static [&'static str]>,
//  None        → 세션 표면을 열지 않는다
//  Some(&[])   → 필터 없음(전 세션 도구) — `tools` 와 같은 규약
//  Some(목록)  → 그 목록만
  • SESSION_READ_TOOLS 상수: 문서를 바꾸지 않는 8종(hwp_open/hwp_doc_text/hwp_doc_info/hwp_doc_fields/hwp_doc_tables/hwp_doc_search/hwp_doc_render_page/hwp_close). hwp_doc_render_page 는 SVG 를 쓰지만 대상이 호출자가 지정한 새 산출물이지 원본 문서가 아니라 조회 축에 남겼습니다.
  • served_toolshandle_tool_call 이 같은 판정 함수(allows_session_tool)를 씁니다. 둘이 갈라지면 경계가 다시 뚫리므로 배선을 하나로 모았습니다.
  • capabilities --mcp --profilesessionTools 를 실어 선언이 실물과 같아지게 했습니다. session: false 인 프로필은 null 입니다.

프로필별 결과: 행정서식·개발통합 → 전 세션 도구(변화 없음), 아카이브검색 → 조회 8종, 나머지 4종 → 세션 없음(변화 없음).

AFTER — 같은 시나리오

tools/list 개수: 15          (무상태 7 + 조회 세션 8)
쓰기 도구 노출: []
hwp_doc_save 호출 결과 isError = True
  | 현재 프로필에서는 세션 도구를 제공하지 않습니다: hwp_doc_save

회귀 가드 2종

  • readonly_profile_serves_no_session_write_tools목록과 호출 두 층을 함께 봅니다. 조회 도구(hwp_open/hwp_doc_search)는 열려 있어야 하고(이 프로필의 레시피가 씁니다), 쓰기 4종은 tools/list 에 없고 tools/call 로도 안 들어가야 합니다.
  • capabilities_declares_the_session_tools_it_actually_serves — 선언=실물. 매니페스트로 도구 정의를 자동 생성하는 소비자(cli_json_pipeline_guide.md 가 문서화한 경로)가 실물과 다른 표면을 얻지 않도록 고정합니다.

검증

  • cargo test --test agent_profile_router_contract7/7 (신규 2종 포함)
  • mcp_server_contract 22/22 · mcp_session_edit_contract 6/6 · cli_json_contract 5/5 — 무회귀
  • cargo clippy --profile release-test --bin rhwp — 경고 0 / cargo fmt --check — 통과
  • 실기: --profile 아카이브검색 로 stdio 구동해 before/after 대조

호환성

session: boolpub 필드였으므로 형식상 파괴적 변경이지만, agent_profiles 를 소비하는 곳은 저장소 안에 main.rs·mcp_serve.rs 둘뿐이고 둘 다 이 PR 에서 갱신했습니다. 봉투 쪽은 profile.session(불리언)이 그대로 남고 sessionTools추가되므로 기존 소비자는 계속 파싱됩니다.

🤖 Generated with Claude Code

프로필의 세션 축이 bool 하나라, 조회 전용 직무가 세션을 쓰려면 편집·저장 4종까지
통째로 열렸다. 실측: --profile 아카이브검색(허용 도구 7종에 쓰기 도구 0개)으로
hwp_open → hwp_doc_save 를 이어 원본 문서를 덮어썼다(31232 → 18432 바이트).
tools/list 는 19종을 냈고 capabilities --mcp --profile 은 7종을 선언해 선언과
서버도 어긋나 있었다.

- AgentProfile.session: bool → session_tools: Option<&[&str]>
  (None=세션 없음, Some(&[])=필터 없음, Some(목록)=그 목록만 — tools 와 같은 규약)
- SESSION_READ_TOOLS 상수: 문서를 바꾸지 않는 8종. render_page 는 새 산출물만
  쓰므로 조회 축에 남긴다
- served_tools 와 handle_tool_call 이 **같은 판정 함수**(allows_session_tool)를 쓴다
  — 목록에서 뺀 도구를 호출로 우회할 수 없다
- capabilities --mcp --profile 에 sessionTools 선언 추가 (선언=실물)
- 계약 테스트 2종: 목록·호출 두 층 모두 쓰기 도구 차단, 자기서술 일치

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jangster77
jangster77 self-requested a review August 1, 2026 14:17
@jangster77 jangster77 closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants