AI에게 내 게이트를 뚫게 하기

Codex가 찾은 게이트 우회 경로와 각 경로를 막기 위해 추가한 검사

Claude가 짠 게이트 전체를 Codex에 넘기고 메인 세션의 우회 경로를 찾도록 요청했다. Codex가 제시한 경로는 기존 테스트에 없던 사례였다.

우회는 파서, 면제 조건, 킬스위치, 게이트 자체, 카운터 다섯 자리에 걸쳐 있었다.

1. 개행으로 파싱 필드 밀기

01편에서 본 대로, 게이트는 도구 호출 JSON을 파싱해 탭으로 이어 붙인 한 줄을 만들고 cut으로 필드를 나눈다. 여기서 명령 안에 개행이 들어 있으면 그 개행이 파싱 결과에도 남는다. 뒤따르는 필드가 다음 줄로 밀려나면서 agent_id 자리에 엉뚱한 값이 들어가고, 서브에이전트 면제가 잘못 발동할 수 있다.

Bash 게이트는 두 겹으로 막는다. 파싱할 때 명령 안의 개행을 SOH 문자 (\x01)로 바꿔서 한 줄로 만들고, 필드를 나눈 뒤 다시 개행으로 되돌린다. 그리고 파싱 결과에 진짜 개행이 남아 있으면 조작으로 보고 차단한다.

case "$_parsed" in
  *$'\n'*)
    printf 'orchestration gate: unexpected multiline parser output; failing closed.\n' >&2
    exit 2
    ;;
esac

파일 게이트에는 인코딩 없이 이 가드만 붙였다. 파일 경로에 개행이 들어올 일은 명령처럼 흔하지 않지만, 들어오면 같은 방식으로 필드가 밀린다. 위 case 블록은 두 스크립트에 같은 문장으로 들어가 있다.

2. codex 접두사에 명령을 얹기

Bash 게이트는 codex로 시작하는 명령을 면제한다. 위임 경로가 항상 작동해야 하기 때문이다. 처음 판은 접두사만 봤다. codex로 시작하기만 하면 그 뒤에 무엇이 붙든 통과했다.

codex --version; printf x > /tmp/pwn

codex --version으로 시작하지만 세미콜론 뒤에 파일 쓰기가 붙어 있다. 접두사만 보는 면제는 이걸 그냥 통과시킨다.

면제 조건을 좁혔다. codex로 시작하고, 명령 치환($(...), 백틱)이 없고, 따옴표를 걷어낸 나머지가 안전한 문자만 남을 때에만 면제 한다. 세미콜론이나 리다이렉트가 붙은 복합 명령은 면제에서 빠져 뒤의 리다이렉트 검사로 넘어간다.

3. 킬스위치 파일을 명령으로 만들기

게이트는 ~/.claude/orch-gate-off 파일이 있으면 스스로 꺼진다. 사람이 켜고 끄라고 둔 스위치다. 그런데 메인 세션이 그 파일을 명령으로 만들면 게이트를 자기 손으로 끌 수 있다.

touch .claude/orch-gate-off

이 명령은 리다이렉트도 tee도 아니라서 파일 쓰기 패턴 검사에 안 걸린다. 그래서 별도 검사를 앞에 뒀다. 명령에 orch-gate-off 문자열이 있고 touch, mkdir, ln, truncate 같은 파일 생성 토큰이 함께 있으면 차단한다. 반대로 파일을 만들지 않고 이름만 언급하는 읽기 명령(cat, grep)은 통과시킨다.

Codex는 따옴표를 끼워 넣어 문자열 검사를 피하는 방법도 제시했다.

touch .claude/orch-gate-o''ff

orch-gate-o''ff는 셸이 실행할 때 빈 따옴표가 사라져 orch-gate-off가 된다. 원문 그대로 문자열을 찾으면 이걸 놓친다. 그래서 원문과, 따옴표를 걷어낸 문자열 두 벌 모두에서 센티널을 확인한다. 따옴표를 걷어내면 orch-gate-off가 다시 조립되므로 여기서 걸린다.

4. 게이트 자체를 고치기

게이트 스크립트나 설정을 직접 고쳐도 제한을 우회할 수 있다. 파일 게이트에게 자기 자신을 무력화하는 편집을 시키면 된다. 서브에이전트는 면제되므로 이 검사는 서브에이전트 면제 다음, 코드 확장자 필터보다 앞에 둬야 한다. 스크립트는 .sh라 코드로 걸리지만, 설정 파일은 .json이라 확장자 필터를 그냥 통과하기 때문이다.

case "$_target_path" in
  *'.claude/hooks/'*|*'.claude/settings.json'|*'.claude/settings.local.json'|*'orch-gate-off'*)
    printf 'Main-session write to orchestration gate infrastructure blocked...\n' >&2
    exit 2
    ;;
esac

경로를 이름만으로 비교하면 .claude//hooks나 ./.claude/./hooks처럼 슬래시를 끼워 넣어 피할 수 있다. 그래서 비교 전에 /./와 연속 슬래시를 하나로 접는다. ..가 섞인 상위 경로 이동은 POSIX 셸에서 정확히 풀기 어려워서, ..와 .claude가 함께 보이면 조작으로 간주하고 차단한다.

Bash 게이트 쪽에서는 같은 목적을 다르게 노렸다. git restore로 부모 디렉터리의 .claude를 되돌려 게이트를 이전 상태로 돌리는 방법이다. git이 파일을 쓰는 하위 명령(restore, checkout, reset 등)으로 불리고 명령에 .claude가 섞이면 차단한다. 반대로 git grep이나 git log는 읽기 전용이라 통과한다.

5. 잠금이 실패하면 세지 않기

편집 카운터는 잠금을 잡고 값을 올린다. 처음 판은 잠금을 못 잡거나 값을 못 쓰면 그냥 진행했다. 편집을 세지 않고 통과시킨 것이다. 이러면 잠금을 일부러 막는 것만으로 카운터를 0에 묶어둘 수 있다.

잠금을 못 잡거나 카운터를 못 쓰면 차단하도록 바꿨다.

if ! printf '%s' "$_count" > "$counter_file"; then
  printf 'orchestration gate: could not persist edit counter; failing closed.\n' >&2
  exit 2
fi

잠금을 못 잡거나 카운터 파일을 못 쓰면 차단한다. 파서가 없을 때와 같은 처리다.

우회 테스트 뒤 남은 제한

파서 조작, 면제 조건과 킬스위치 검사는 Bash 게이트에 넣었다. 인프라 보호, 카운터 실패 처리와 개행 가드는 파일 게이트에 넣었다. 발견한 우회는 셸 명령에 더 많이 몰려 있었다. 이 결과를 반영해 Bash 게이트 주석에는 “최선 노력이고 보안 경계가 아니다”라고 적었다.

확장자를 바꿔 저장하는 우회는 지금도 열려 있다. 스크립트 첫 주석에 이렇게 적어 뒀다.

BEST-EFFORT ANTI-DRIFT GATE. Renaming a code file to an excluded extension
(e.g. .json) bypasses the extension filter.

수정한 자리는 전부 회귀 테스트가 됐다. 우회 문자열 하나마다 “이건 차단”, 그 문자열과 비슷하지만 정상인 명령마다 “이건 통과”를 짝으로 넣어 42개가 됐다. git grep 'restore'가 통과하고 git restore가 차단되는지, 센티널을 언급만 하는 명령이 통과하는지까지 확인한다. 게이트 번들에 넣어 둔 tests/gate-tests.sh를 실행하면 그대로 돌려볼 수 있다.

이 테스트에서는 게이트를 작성한 Claude와 다른 모델인 Codex가 기존 테스트에 없던 우회 경로를 찾았다. 이 결과를 다른 코드 검토에도 적용하려면 작성자와 검토자를 어떻게 나눌지 별도의 규칙이 필요했다.

참고

Comments

댓글

    이미지 확대