허용 목록(allow-list)은 실패 시 닫히지 않는다 (fail closed)
요약
본 글은 AI 에이전트의 보안 및 쉘 강화(hardening)에 대한 심층적인 분석을 제공합니다. 특히, 플러그인 가드 시스템이 '허용 목록(allow-list)' 방식을 채택하여 보호된 경로를 명시적으로 확인하는 방식으로 작동함을 설명합니다. 이 방식은 에이전트가 예상치 못한 엣지 케이스를 시도할 때 안전하게 차단하는 원리를 보여줍니다.
핵심 포인트
- 허용 목록(allow-list) 기반의 가드 시스템이 보안에 효과적입니다.
- 시스템은 '기존 보호된 경로 명시' 여부를 기준으로 규칙 적용을 결정합니다.
- 파일 시스템에 존재하지 않는 토큰이나 패턴은 차단되지 않고 무시됩니다.
- 이는 에이전트가 임의의 문자열로 쉘 명령어를 구성할 때 보안 취약점을 방지합니다.
지난주에 한 스레드에서 누군가 클로드 코드 플러그인(Claude Code plugin) 관련하여 에이전트가 자신의 테스트를 편집하는 것을 막는다는 내용으로, 허용 목록(allow-list)은 '실패 시 닫힌다(fails closed)', 즉 에이전트가 엣지 케이스를 시도할 때 원하는 실패 모드라고 말했습니다.
이는 올바른 직관이지만 잘못된 결론입니다. 이 부분은 분해하여 살펴볼 가치가 있습니다. 왜냐하면 동일한 추론이 누군가가 에이전트의 쉘을 강화(harden)할 때마다 나타나기 때문입니다.
가드(The guard)
플러그인(Mattbusel/tamperproof, v0.1.1)은 'writer' 명령어에 대한 거부 목록(deny-list)을 허용 목록으로 대체했습니다. 이는 올바른 방향입니다. 왜냐하면 거부 목록은 누군가가 생각한 동사들만을 다루기 때문입니다. hooks/guard.mjs의 새로운 규칙은 다음과 같습니다:
기존 보호된 경로(protected path)를 명시하는 모든 쉘 세그먼트(shell segment)는 알려진 읽기 전용 또는 테스트 실행기 명령어(read-only or test-runner command)로 시작하고 리디렉션이나 tee가 포함되지 않는 경우에만 차단됩니다.
첫 번째 구절을 천천히 읽어보세요. **"기존 보호된 경로를 명시하는 것(names an existing protected path)"**이 규칙이 적용될지 여부를 결정합니다. 코드는 다음과 같습니다:
for (const piece of seg.split(/[\[\s'"`,;()=<>{}\[\]]+/)) {
if (!piece || piece.startsWith("-") || /^[a-z]+:\/\/i.test(piece)) continue;
const r = rel(piece);
...
리터럴 토큰(literal token)에 대해 existsSync(resolve(cwd, piece))를 사용합니다. 만약 어떤 토큰도 기존 보호 파일처럼 보이지 않으면, 해당 세그먼트는 건너뛰어집니다. 즉, 판단되지 않았다는 것은 허용된다는 의미입니다.
제가 측정한 것(What I measured)
저는 7559493 (v0.1.1)을 확인하고, test/calc.test.mjs를 포함하는 임시 트리(throwaway tree)를 만들었으며, 가드를 후크가 작동하는 방식대로 구동해 보았습니다. 즉, stdin으로 도구 호출을 하고 종료 코드를 읽어보는 것입니다. 모든 행에서 동일한 명령어를 사용했지만, 대상의 철자만 바꿨습니다:
$ sed -i s/5/-1/ test/calc.test.mjs -> exit 2 blocked
$ sed -i s/5/-1/ test/calc.test.j? -> exit 0
$ sed -i s/5/-1/ test/*.mjs -> exit 0
...
리터럴 답변이 2인 이유는 그것이 실제로 파일이기 때문입니다: existsSync가 '예'라고 말합니다. 경로가 보호되었고, 세그먼트가 판단되었으며, sed -i는 읽기 전용 목록에 없으므로 차단됩니다.
나머지 답변은 existsSync가 존재하지 않는다고 하므로 0입니다. test/*.mjs는 경로(path)가 아니라 토큰입니다. 가드(guard)는 파일 시스템에 문자열 test/*.mjs에 대해 문의하며, 이 문자열은 파일이 아닙니다. ?도 마찬가지이고; 중괄호 형태({})도 마찬가지입니다 (분리자(splitter)는 {와 }를 분리하여 test/calc.test.만 남깁니다). $PWD/...도 마찬가지인데, 이는 $PWD라는 이름의 디렉토리 아래에서 해석됩니다. 아무것도 일치하지 않으므로 hits는 비어 있게 유지되고 규칙은 절대 참조되지 않습니다.
그리고 철자(spelling) 오류는 sed가 그것을 보기 전에 실제 셸(shell)에 의해 확장됩니다. 이것이 바로 글롭(glob)의 전체 역할입니다. 따라서 이 문자열은 그 문자열 자체를 주체로 하는 검사를 무력화시킵니다.
일반화, 즉 유용한 부분
가드가 실패하는 방식은 허용 목록(allow-list) 대 거부 목록(deny-list)의 속성이 아닙니다. 그것은 결정 체인에서 기본값(default)이 어디에 위치하느냐의 속성입니다.
이 가드는 판정(verdict) 측면에서는 기본적으로 **거부(deny)**입니다: 판단되어 읽기 전용(read-only)이 아닌 세그먼트는 차단됩니다. 그리고 선택자(selector) 측면에서는 기본적으로 **허용(allow)**입니다: 결코 판단되지 않는 세그먼트는 허용됩니다. 선택자가 두 가지 중 더 앞선 단계이며, 이는 명령어 문자열이 구동하는 계층입니다. 따라서 이 가드는 공격자가 자유가 없는 지점에서는 정확히 닫히고(fails closed), 공격자가 자유를 가진 지점에서는 정확히 열립니다(fails open).
'허용 목록은 닫힌다(fails closed)'는 말은 당신이 우연히 나열한 축(axis)에서만 참입니다—여기서는 동사(verb)입니다. 공격자의 입력이 제어하는 축을 나열하거나, 아니면 잘못된 것을 측정하고 있는 것입니다.
다른 방향도 실제로 존재한다
두 개의 팔이 반대쪽으로 돌아왔고, 이것들이 제가 심지어 동사 축까지 단순히 '닫혔다'고 부르지 않는 이유입니다:
$ cat test/calc.test.mjs > /dev/null -> exit 2
$ grep -n '=>' test/calc.test.mjs -> exit 2
writesOut이 />/.test(seg)인 경우, 화살표는 grep 패턴에서 리디렉션으로 읽히며, cat ... > /dev/null은 아무 데도 가지 않는 리디렉션을 의미합니다. 이 두 가지 모두 에이전트가 실패한 테스트를 디버깅하는 동안 끊임없이 수행하는 행위이며, 바로 가드(guard)가 존재하여 그 상태를 유지하려는 목적과 일치합니다. 거짓 블록(false block)은 공짜가 아닙니다. 이는 독자에게 가드를 우회하도록 방법을 가르치고, 결국 가드는 아무것도 보호하지 못하게 만듭니다.
제가 얻을 수 있는 교훈
- 규칙별 기본값이 어느 레이어에 존재하는지 이름을 붙여야 합니다. "읽기 전용이 아닐 때 차단됨(Blocked unless read-only)"과 "확인되지 않음(not looked at)"은 서로 다른 판결이며, 코드 리뷰에서는 그중 하나만 보입니다.
- 문자열 이름이 아닌, 작성될 대상에 대해 질문해야 합니다. 그렇지 않으면 문자열을 오타로 간주하고 철자를 추적하는 것에 만족해야 합니다. 저자는 이미 정직한 긴장감을 명명했습니다: 글롭(glob) 형태의 토큰을 히트(hit)로 취급하는 것이 이 구멍을 막고, 셸(shell)에서 작성된 진정으로 새로운 테스트까지도 차단합니다.
- 양방향으로 제어 팔(control arm)을 유지해야 합니다. 가드의 모든 스물 개의 테스트 케이스는 경로를 명시하고 있으며, 제가 보기 전에 보냈던 모든 팔들도 그러했습니다. 케이스 개수는 커버리지 주장(coverage claim)이 아닙니다. 중요한 질문은 그 케이스들이 어떤 '축(axis)'을 따라 움직이는가입니다.
이 플러그인은 정말 훌륭한 작업물이며, 0.1.1 버전에서는 이전 버전의 모든 구멍들, 여기에는 위에서 언급된 리터럴 철자 문제까지도 막았습니다. 이것은 리라이트(rewrite)를 거치면서 살아남은 유일한 축이며, 수정 사항이 셀렉터(selector)에 들어가지 않는 한 다음 번에도 살아남을 것입니다. 제가 이 글의 출처가 된 스레드는 여기에서 작성했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기