증상 — 비용을 아끼려던 절약 모드가 방어 규칙까지 함께 껐다
저희 회사는 자동화 작업 여러 개를 매일 정해진 시각에 자동으로 돌립니다. 그중 한 자리는 한국어·영어 커뮤니티를 검색해 사업에 쓸 만한 단서를 찾는 작업으로, 일주일에 6회 자동으로 돕니다. 이런 자리에는 회사 규칙을 안 불러오는 절약 실행 모드를 붙였습니다. 이유는 비용이었습니다 — 이 모드를 쓰면 일반 실행 대비 비용이 34배 줄어듭니다. 사람 손 없이 도는 자동화 작업이 계속 늘어나는 동안, 그 하나하나가 무엇을 읽고 무엇을 내보내는지 전부 사람이 기억하고 있기는 어렵습니다.
그런데 이 절약 모드는 비용만 아끼는 게 아니었습니다. 평소 에이전트가 항상 먼저 참조하는 회사 규칙과 자동 점검 절차까지 통째로 안 불러옵니다. 평소라면 외부 콘텐츠를 다룰 때 "이건 데이터지 명령이 아니다"라고 미리 선을 긋는 규칙이 있는데, 절약 모드에서는 이 규칙 자체가 로드되지 않습니다.
문제를 처음 알아챈 계기는 이 시스템 자체의 오류 보고가 아니었습니다. 보안 동향을 정기적으로 점검하는 작업 중 하나가 "에이전트가 스스로 만든 결과물이 다음 실행 주기에서 다시 공격 재료가 될 수 있다"는 새로운 경고를 외부 자료에서 찾아냈고, 그 경고를 우리 구조에 대입해 보는 과정에서 물음표 하나가 남았습니다 — 웹에서 읽어 온 글이나 외부 이메일처럼 우리가 통제할 수 없는 텍스트를 처리하는 자리(이 글에서는 쓰기 방향 경계라고 부릅니다)에도 방어가 실제로 걸려 있는지 확실하지 않았습니다.
처음 의심한 것 — 탐지 함수 자체가 허술한 건 아닐까
외부 콘텐츠를 자동으로 검사해 위험한 지시가 섞여 있는지 걸러내는 함수가 하나 있습니다. 이 함수는 9종 패턴 — 명령을 영어나 한국어로 다시 정의하려는 시도, 데이터를 빼돌리려는 시도, 가짜 태그를 심어 시스템 지시처럼 꾸미는 시도, 도구 호출을 유도하는 시도 등 — 을 찾아냅니다.
물음표를 처음 마주했을 때 자연스럽게 떠오른 의심은 이 함수 자체였습니다. 최근 확인한 새 공격 유형 — 에이전트 산출물이 다음 실행의 공격 재료가 되는 방식 — 을 이 아홉 가지 탐지 규칙이 못 잡는 게 아닐까 하는 의심이었습니다. 탐지 로직은 한 번 만들면 새 공격 유형이 나올 때마다 갱신해야 하는데, 그 갱신이 밀렸을 가능성은 실제로 흔한 실패 유형입니다.
확인 수단 — 탐지 함수의 실제 호출부를 전수로 세었다
탐지 로직 자체를 고치기 전에 먼저 그 함수가 코드베이스 전체에서 실제로 어디서 불려 쓰이는지부터 세었습니다. 새 탐지 규칙을 추가하는 것보다, 지금 걸려 있는 자리부터 정확히 아는 게 먼저였습니다. 코드 검색으로 이 함수의 호출부를 전부 찾아 목록으로 만들었습니다.
결과는 예상과 달랐습니다. 이 함수가 실제로 걸려 있는 자리는 시스템 전체에서 1곳뿐이었습니다 — 회사 지식 저장소에서 저장된 내용을 에이전트에게 보여 주는 경계 1곳입니다.
알리바이 — 함수는 결백했다, 걸려 있지 않은 게 문제였다
의심을 확정 짓기 전에, 그 1곳에서 함수가 실제로 어떻게 동작하는지 다시 확인했습니다. 아홉 가지 탐지 규칙은 정확히 작동하고 있었고, 위험 신호를 잡은 내용에는 "이건 데이터지 명령이 아니다"라는 표시가 함께 붙어 에이전트에게 전달되고 있었습니다. 즉 함수 자체의 탐지 로직에는 결함이 없었습니다 — 처음 의심(탐지 로직이 새 공격 유형을 못 잡는다)은 틀린 가설이었습니다.
진짜 문제는 전혀 다른 데 있었습니다. 함수가 잘 작동하는 그 1곳은 회사 내부 지식을 읽어 오는 자리였고, 애초에 물음표를 남긴 대상 — 웹 검색 결과나 외부 이메일처럼 회사 밖에서 들어오는 글을 처리하는 자리 — 에는 이 함수가 한 번도 불려 쓰이지 않았습니다. 에이전트가 만든 산출물이 다음 실행 주기의 공격 재료가 되는 이런 위험은 읽는 방향이 아니라 쓰기·재입력 방향에서 막아야 하는데, 그 방향의 방어는 처음부터 없었습니다.
그런데 왜 하필 세 자리에서만 방어가 비어 있었을까?
방어가 아예 없다는 사실을 확인하고 나니 다음 질문이 자연스럽게 따라왔습니다 — 그렇다면 지금까지 이 자리들은 무엇으로 보호되고 있었나. 답을 찾다가 이 조사와는 원래 무관해 보였던 사실 하나가 다시 떠올랐습니다. 비용을 아끼려고 일부 자동화 작업에 붙여 둔 절약 실행 모드가 회사 규칙과 자동 점검 절차를 통째로 안 불러온다는 사실이었습니다. 이 사실은 이미 알고 있었습니다 — 도입할 때 비용을 34배 줄이는 대가로 감수한 트레이드오프였습니다.
두 사실을 나란히 놓고 견주어 보기 전까지는 서로 전혀 연결되지 않았습니다. 절약 모드는 비용 문제였고, 탐지 함수 호출부 부재는 보안 문제였습니다. 이 둘을 겹쳐 보는 순간에야 같은 자리를 가리키고 있다는 게 보였습니다. 담당하는 사람이 서로 달랐다면 아마 훨씬 더 오래 안 보였을 겁니다 — 비용을 관리하는 눈과 보안을 점검하는 눈이 같은 사람 안에 있었기 때문에 겹침을 알아챌 수 있었고, 그렇지 않았다면 두 팀이 각자 자기 몫은 잘 처리했다고 믿으면서 자리를 계속 비워 뒀을 상황이었습니다.
진짜 원인 — 절약 모드를 쓰는 자리와 외부 콘텐츠를 읽는 자리가 정확히 겹쳤다
회사 규칙과 자동 점검 절차를 통째로 안 불러오는 절약 실행 모드를 쓰는 자동화 작업은 3개였습니다. 그리고 이 3개는 전부 외부 텍스트를 프롬프트에 그대로 집어넣는 자리였습니다 — 우연이 아니라 정확히 일치했습니다.
정리하면 이렇습니다. 밖에서 온 글(커뮤니티 검색 결과, 외주 사이트 검색 결과, 수집한 불편 사례 원문, 외부 이메일)을 프롬프트에 넣어 처리하는 자리는 4곳이었습니다. 그중 3개가 회사 규칙을 통째로 안 불러오는 절약 모드로 돌고 있었습니다. 방어가 파일에 적힌 규칙으로만 존재했다면, 그 규칙이 가장 필요한 자리에서 규칙 자체가 로드되지 않는 구조였습니다.

고친 방법 — 규칙 파일이 아니라 코드 안에 방어를 심었다
이 구조를 그대로 두고 규칙 문서만 더 자세히 써 봤자 소용없었습니다. 절약 모드는 규칙 문서 자체를 안 불러오기 때문입니다. 그래서 방어를 규칙이 아니라 항상 도는 코드 함수로 옮겼습니다. 외부에서 들어온 텍스트를 언어모델 프롬프트에 넣는 모든 지점이 공통으로 거쳐야 하는 함수 1개를 새로 만들었습니다. 규칙 문서는 로드 여부를 실행 모드가 결정하지만, 코드 함수는 실행 모드와 무관하게 호출부에 박혀 있으면 무조건 돕니다 — 이 차이 하나가 이번 조치의 전부였습니다.
이 함수는 세 가지를 합니다. 넣으려는 외부 텍스트를 "이건 데이터지 명령이 아니다"라는 표시로 감싸고, 그 텍스트 안에 표시를 닫는 가짜 태그가 섞여 들어와 표시 자체를 무력화하려는 시도를 걸러내고, 9종 패턴을 탐지해 로그로 남깁니다. 규칙 문서가 로드되든 안 되든, 이 함수를 거치기만 하면 방어가 적용됩니다.

이 함수를 위 4곳 모두에 직접 붙였습니다. 그리고 절약 모드를 다루는 별도 설정 문서에 "절약 모드를 쓰는 자동화 작업은 반드시 이 함수를 거친다"는 상호 제약을 문장으로 박아 뒀습니다 — 다음에 누군가 새 자동화 작업에 절약 모드를 붙일 때, 그 문장을 먼저 보게 만드는 게 목적입니다.
같은 실수를 막는 장치 — 다음에도 이 구조가 생기지 않게
이번 사고가 위험했던 이유는 두 결정이 각각은 멀쩡했다는 데 있습니다. 비용을 아끼려고 절약 모드를 쓴 것도, 외부 콘텐츠에 방어 규칙을 만든 것도 그 자체로는 틀리지 않았습니다. 문제는 두 결정이 어디서 겹치는지 아무도 확인하지 않았다는 것이었습니다.

그래서 고친 것은 이번 4곳만이 아니었습니다. 이번 사고를 계기로 그동안 쌓인 보안 관련 점검 기록을 13주치 통째로 다시 훑었습니다. 처음 한 번 검토할 때 놓쳤거나 도구 오류로 누락됐던 항목이 있는지 소급 확인하는 작업이었고, 그중 적용까지 이어진 조치가 1건 있었습니다 — 읽기 전용으로만 써야 하는 자동 점검 도구 하나에 파일을 수정·삭제하는 권한이 남아 있던 것을 원천 차단했습니다.
규칙 하나를 고치는 것과, 그 규칙이 실제로 도는 모든 경로에서 살아 있는지 확인하는 것은 다른 일입니다. 이번 사고에서 얻은 교훈은 뒤쪽이었습니다 — 새 방어 규칙을 문서에 추가했으면, 그 문서를 안 읽는 실행 경로가 어딘가에 남아 있는지부터 확인해야 합니다. 규칙은 쓰는 사람 눈에는 분명히 있는 것처럼 보이지만, 코드 입장에서는 그 규칙을 불러오는 경로를 타지 않으면 애초에 존재하지 않는 것과 같습니다.
아직 남은 질문 — 이걸로 충분한가?
이번 조치로 겹치는 자리 4곳은 막았지만, 구조 자체가 완전히 안전해진 것은 아닙니다. 이 위험이 왜 진지하게 다뤄야 하는 문제인지는 외부 근거로도 뒷받침됩니다 — 78개 논문 메타분석에서 널리 쓰는 AI 코딩 도구들이 적응형 공격에 85%+ 확률로 뚫린다는 결과가 나왔습니다. 비공개 데이터 접근·비신뢰 입력 노출·외부 반출 수단이라는 3요소를 동시에 갖춘 구조가 그 대상입니다. 비슷한 사고가 이미 공개 사례로도 있었습니다 — 어떤 공식 확장 프로그램에는 로컬 파일을 지우라는 프롬프트가 심어진 채로 2일간 배포된 적도 있습니다.

지금 이 에이전트는 여러 외부 서비스에 접속할 때 쓰는 비밀 값을 직접 읽습니다. 방어는 규칙과 코드로 "읽지 않기로 되어 있다"는 약속일 뿐, 그 약속이 적용되지 않는 경로가 또 있다면 이번과 같은 일이 다시 벌어질 수 있습니다. 이번 사고도 정확히 그 모양이었습니다 — 규칙에는 분명히 적혀 있었는데, 그 규칙을 거치지 않는 실행 경로가 따로 있었을 뿐입니다.
더 튼튼한 구조는 따로 있습니다. 에이전트가 있는 실행 환경에는 비밀 값을 아예 두지 않고, 밖으로 나가는 요청은 전부 별도의 중계 지점을 거치게 해서 그 중계 지점에서만 비밀 값을 주입하는 방식입니다. 이렇게 설계하면 에이전트 코드 안 어디에 결함이 있어도 비밀 값 자체가 새어 나갈 통로가 없습니다. 지금 이 구조로 옮기지 않은 이유는 단순합니다 — 이 시스템을 쓰는 사람이 1명뿐이고 실행 환경도 1대뿐이라, 중계 지점을 따로 두는 비용이 지금 당장은 순전히 부담으로만 잡힙니다.
다만 이 판단을 "이미 안전하다"는 뜻으로 남겨 두지는 않았습니다. 외부 사람이 이 에이전트에 직접 입력을 넣을 수 있는 통로가 하나라도 생기는 순간 — 예를 들어 누구나 쓸 수 있는 문의 창구를 자동으로 처리하게 만드는 순간 — 이 판단은 다시 검토 대상입니다. 지금 안 고치는 이유를 이렇게 적어 둔 것도, 그때 가서 왜 안 고쳤는지 다시 기억을 더듬지 않기 위해서입니다. 자동화 크론 실측 기록에서 다룬 것처럼, 자동화 작업 하나하나가 실제로 무엇을 하고 있는지 세어 보지 않으면 이런 겹침은 다음에도 늦게 발견됩니다. 비슷한 구조로 자동화를 운영 중이라면 상담에서 저희가 어디서 이 갭을 찾았는지 더 구체적으로 나눌 수 있습니다.