스팸 봇들에게 우리를 이기는 법을 알려줄 뻔했던 순간
요약
스팸 탐지 시스템 구축 중 보안 시스템의 출력값이 공격자에게 우회 방법을 알려주는 공격 표면이 될 수 있음을 발견한 사례를 다룹니다. 탐지 로직과 외부 공개 정보를 분리하여 보안성을 강화하는 설계 원칙을 제시합니다.
핵심 포인트
- 보안 시스템의 상세 출력값은 공격자에게 유용한 명세서가 될 수 있음
- 디버깅을 위한 상세 정보와 사용자에게 노출되는 정보를 엄격히 분리해야 함
- 탐지 로직과 탐지 공개(disclosure)는 서로 다른 시스템으로 설계해야 함
- 공개 사유는 고정되고 일반적인 어휘를 사용하여 구체적인 패턴 노출을 방지함
Open Feed Network (Candor Network)의 창립자인 Ronny Cruz Alvarez 작성. 1인 창립자, 하나의 프로덕션 플랫폼 운영자, 그리고 거절 메시지에 무엇을 담을 수 있는지에 대해 많은 의견을 가진 사람.
나를 확신시킨 버그
2주 전, Fediverse의 최근 가입 스팸 파동 속에서 가입 심사 도구를 구축하던 중, 나는 내 탐지 엔진에서 이미 알고 있었어야 할 사실을 가르쳐 주는 버그를 발견했습니다. 그것은 바로 보안 시스템의 출력값(output)이 공격 표면(attack surface)이 될 수 있다는 점입니다.
내가 만드는 모든 것의 배후에서 경계 방어 분류기(perimeter-defense classifier) 역할을 하는 엔진인 Sentinel은 패턴 매처(pattern matcher) 뱅크를 사용하여 스팸 가입 텍스트를 플래그(flag) 처리합니다. 암호화폐 권유, 일반적인 템플릿 형태의 서두 등이 이에 해당합니다. 패턴이 일치할 때, 호출자(caller)에게 반환되는 거절 사유는 다음과 같은 형태였습니다:
jsreason: spam phrase matched: /crypto|investment.{0,10}opportunity/i
읽기 쉽고, 디버깅하기 좋습니다. 하지만 만약 이 메시지가 관리자의 비공개 로그에 머물지 않고 거절당한 사람에게 전달되었다면 — 어떤 단어를 피해야 하는지에 대한 완벽하고 기계가 읽을 수 있는(machine-readable) 명세서가 되었을 것입니다. 나는 스팸 필터를 만든 것이 아닙니다. 상자 위에 우회 방법이 인쇄되어 있는 스팸 필터를 만든 셈이었습니다.
아무것도 충돌하지 않았습니다. 테스트 중에는 아무런 문제도 없어 보였습니다. 정규 표현식(regex)은 매번 완벽하게 제 역할을 수행했습니다 — 조용히 정답지를 건네주면서 말이죠.
이것은 드문 실수가 아닙니다. 탐지 시스템을 빠르게 구축할 때 발생하는 일반적인 실패 모드입니다. 자신을 위한 디버깅을 위해 도구를 만들다 보면, "나에게 도움이 되는 것"과 "공격자에게 도움이 되는 것"이 종종 동일한 문자열이라는 사실을 잊게 됩니다.
불변의 원칙
나는 당일에 패치를 완료했지만, 패치 자체보다 그 이후에 작성한 규칙이 더 중요했습니다. 그리고 그 규칙이 현재 Sentinel을 실제로 지배하고 있습니다:
탐지 로직(detection logic)과 탐지 공개(detection disclosure)는 서로 다른 시스템이며, 오직 그중 하나만이 호출자를 마주해야 한다.
내부적으로는 모든 판정(verdict)이 어떤 계층이 작동했는지, 어떤 패턴이 일치했는지, 정확한 가중치(weight)가 무엇인지에 대한 전체적인 추론 과정을 여전히 포함하고 있습니다. 이러한 세부 사항은 필수적입니다. 스스로를 포함하여, 감사(audit)할 수 없는 안전 시스템은 신뢰할 수 없는 안전 시스템이기 때문입니다. 하지만 내부적인 정보가 거부된 사용자나 공개 API 응답이 보는 것과 동일한 객체인 것은 결코 아닙니다. 공개적인 사유는 "일회용 이메일 도메인(disposable email domain)", "스팸 문구 감지(spam phrase detected)", "가입 패턴 플래그 지정(signup pattern flagged)"과 같이 고정되고 일반적인 어휘에서 추출됩니다. 이는 특정 정규 표현식(regex), 특정 임계값(threshold), 또는 특정 리스트 항목을 의도적으로 명시할 수 없도록 설계되었습니다. API를 탐색하는 공격자는 무언가가 걸렸다는 사실은 알 수 있습니다. 하지만 그것이 걸리지 않게 하려면 무엇을 바꿔야 하는지는 알 수 없습니다.
이러한 구분은 이제 Sentinel이 실행되는 두 영역 모두에서 핵심적인 역할을 합니다.
두 개의 전선, 하나의 엔진
Sentinel은 경계 방어(perimeter defense)로 시작되었습니다. 즉, 평판(reputation), 속도(velocity), 타이밍의 규칙성(timing regularity) 등을 바탕으로 요청 수준의 점수(request-level scoring)를 매겨, 요청이 애플리케이션에 닿기도 전에 차단하거나 도전 과제(challenge)를 부여할지 결정하는 방식입니다. Sentinel Signup은 동일한 6계층 엔진을 더 좁고 구체적인 문제에 적용한 것입니다. 바로 Fediverse 등록 대기열(registration queues)인데, 지난 7월의 스팸 파동은 자원봉사자가 운영하는 Mastodon 인스턴스들에게 진정한 위기를 가져왔습니다. 일회용 이메일, 데이터 센터 IP, 템플릿화된 "승인 부탁드립니다" 문구, 동일 서브넷 가입 폭주, 그리고 — 스페인어 관리자가 마치 조직적인 러시아어 파동처럼 보이는 공격을 받았다고 설명한 후 이번 주에 추가된 — 스크립트/로케일 불일치 신호(script/locale mismatch signal): 계정의 선언된 로케일과 모순되는 스크립트로 작성된 애플리케이션 텍스트 등이 이에 해당합니다.
동일한 공개 규칙이 양쪽 모두에 적용됩니다. 경계 가드(perimeter guard)는 도전에 직면한 클라이언트에게 "요청 플래그 지정(request flagged)"이라고만 알립니다. 평판, 속도, 타이밍 체크 중 어떤 것이 작동했는지, 혹은 얼마나 작동했는지는 알려주지 않습니다. Sentinel Signup은 인스턴스 관리자에게 "차단: 일회용 이메일 도메인; 데이터 센터 IP; 템플릿 콘텐츠"와 같이 카테고리를 알려주며, 근본적인 패턴 라이브러리를 알려주지는 않습니다. 관리자는 판정을 신뢰할 수 있을 만큼의 충분한 정보를 얻습니다. 반면 봇 운영자는 우회할 수 있는 정보를 전혀 얻지 못합니다.
Fail-to-queue (대기열로 실패 처리), fail-open(오픈 실패) 또는 fail-closed(클로즈 실패)가 아닌 방식
Sentinel Signup이 예외 없이 강제하는 또 다른 규칙은 다음과 같습니다. 만약 스크리닝(screening) 자체에서 오류가 발생한다면 — 종속성 타임아웃(dependency timeout), 조회 실패(lookup failure) 등 무엇이든 — 판정 결과는 '플래그(flag, 의심)'여야 하며, 절대로 '통과(pass)'시키거나 '차단(block)'해서는 안 됩니다.
이는 대부분의 시스템이 구축하지 않는 의도적인 세 번째 선택지입니다. 스팸 필터에서 fail-open(오픈 실패) 방식은 장애가 발생했을 때 보안 구멍이 열리는 것을 의미합니다. fail-closed(클로즈 실패) 방식은 화요일 오후의 사소한 결함이 봇을 차단하는 것만큼이나 실제 신규 사용자들을 강력하게 차단해 버린다는 것을 의미합니다. 등록을 폐쇄하는 것은 이 모든 시스템이 존재하기 전부터 관리자들이 이미 저지르고 있던 패배하는 수였으며, 그 패배를 자동화하는 것은 해결책이 아닙니다. fail-to-queue(대기열로 실패 처리) 방식은 오류가 발생했을 때 인간 관리자에게 30초의 판단 시간을 비용으로 지불할 뿐, 신청자에게는 아무런 비용도 발생시키지 않습니다. 이 실패는 가시적이고 비용이 저렴하며, 어느 방향으로든 잘못된 판정을 만들어내지 않습니다.
이와 짝을 이루는 것은 제가 어디에서나 사용하는 동일한 'fail-loud(실패를 명확히 알리는)' 원칙입니다. 부팅 시 데이터베이스 연결이 누락되었다면, 이는 실행을 거부해야 하는 문제이지, 다음 재시작 시 모든 테넌트(tenant)의 데이터를 조용히 잃어버리게 만드는 메모리로의 조용한 폴백(fallback)이 되어서는 안 됩니다. 저는 서비스가 무엇을 보장할 수 있는지에 대해 거짓말을 하며 실행되느니, 차라리 실행되지 않는 쪽을 택하겠습니다.
정직한 한계
아직 완성되지 않은 부분들은 다음과 같습니다:
정보 공개의 경계는 현재 코드 레이어(code layer)에서 관례(convention)에 의해 강제됩니다. 모든 응답 경로(response path)는 내부 패턴 세트가 아닌 일반적인 어휘를 사용하는지 확인하기 위해 수동으로 검토됩니다. 이는 Candor의 핵심 피드(core feed) 내 저장 레이어(storage layer)가 스크리닝되지 않은 콘텐츠를 구조적으로 수용할 수 없는 것과 같은 방식으로, 아직 구조적으로 강제되지는 않았습니다. 그것이 다음 단계의 강화 작업이며, 그것이 구축될 때까지 "일반적 어휘 전용(generic-only)"은 아키텍처가 보장해 주는 것이 아니라 제가 유지하는 규율입니다.
오늘날의 텍스트 분석은 휴리스틱 (heuristic) 및 규칙 기반 (rule-based) 방식이며, 여기에 기반 엔진 자체의 콘텐츠 점수 산정 (content scoring)이 더해진 형태입니다. 루프(loop) 내에 학습된 분류기 (learned classifier)도 없으며, 가입 심사를 위한 LLM (Large Language Model) 또한 전혀 사용되지 않습니다. 이는 의도적인 결정입니다. 스팸 방지 비용을 지불할 수 없는 인스턴스들이야말로 가장 심하게 공격받는 대상이기 때문에, 심사당하는 인스턴스당 한계 비용 (marginal cost)이 사실상 제로에 가깝게 작동해야 합니다. 인스턴스별로 공개되는 옵트인 (opt-in) 분류기 방식이 설계되어 있으나 아직 구축되지는 않았습니다.
스크립트/로캘 (script/locale) 신호는 새로운 방식입니다. 도입된 지 며칠 되지 않았으며, 하나의 실제 공격 패턴을 바탕으로 합니다. 이 방식은 계정이 특정 로캘 (locale)을 선언했음에도 신청자의 텍스트가 특정 방식으로 그와 모순될 때만 작동하며, 단 한 번의 일치만으로도 자동 거절이 아닌 인간의 검토 (human review)를 유도합니다. 이러한 보수적인 접근은 의도된 것입니다. 빈약한 증거를 바탕으로 잘못된 휴리스틱 (heuristic)을 스스로 학습하게 하느니, 차라리 한동안 작동을 덜 하는 편을 택하겠습니다.
저는 1인 창업자입니다. Sentinel은 5월부터 프로덕션 환경에서 경계 방어 (perimeter defense)를 수행해 왔습니다. Sentinel Signup은 출시된 지 일주일 되었으며, 현재 베타 버전으로서 무료로 운영 중입니다. 이는 합성 테스트 케이스 (synthetic test cases)가 아닌 실제 적대적 트래픽 (adversarial traffic)을 통해, 누군가 비용을 지불하기 전에 시스템이 실제로 어디서 틀리는지 저에게 알려주기 위함입니다.
이러한 순서가 중요한 이유
모든 탐지 시스템은 결국 자신이 탐지하려는 대상으로부터 탐색 (probe)을 당하게 됩니다. 문제는 공격자가 테스트 트래픽을 보내고 당신의 응답을 읽을 것인가 하는 점이 아닙니다. 그들은 즉시 그렇게 할 것이며, 당신의 필터가 존재한다는 사실을 알게 된 것도 바로 그 때문입니다. 진짜 문제는 당신의 응답이 그들에게 무언가를 가르쳐 주느냐 하는 것입니다. 거절 메시지는 비공개 디버깅 로그 (debugging log)가 아닙니다. 메시지가 당신의 프로세스를 벗어나는 순간, 그것은 당신이 시스템을 구축한 목적(공격 차단)을 가진 사람에게 넘겨줄 수 있는 가장 가치 있는 무료 정찰 (reconnaissance) 정보가 됩니다.
만약 당신이 Mastodon 인스턴스를 운영하거나, 현재 가입 대기열 (registration queue) 공격을 받고 있는 서비스를 운영 중이라면, Sentinel Signup은 signup.candortheopenfeednetwork.com에서 베타 기간 동안 무료로 제공됩니다. 만약 정보 공개 경계 (disclosure boundary)가 여전히 누출되는 지점을 저에게 알려주고 싶다면, 저는 그것을 악용하는 사람보다는 당신에게 직접 듣고 싶습니다. 제 이메일은 tips@candortheopenfeednetwork.com이며, 모든 메일에 제가 직접 답변합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기