
환각('슬롭') 취약점 보고서를 신속하게 분류하기 위한 코드 카나리아
honeyslop은 AI 환각("슬롭") 및 검증되지 않은 취약점 보고로 넘쳐나는 오픈소스 프로젝트를 위한 코드 카나리아, 즉 유인책(decoy)입니다. 이러한 적대적 노이즈 주입을 통해 슬롭 스캐너가 카나리아를 입력으로 받아들인 뒤, 이를 기반으로 취약점 "보고서"를 생성합니다. 그 보고서는 스스로 슬롭임을 드러내므로, grep 한 번으로 종결할 수 있습니다.
이것은 농담 삼아 vibe-coding으로 만든 빠른 PoC(프로덕션 등급 아님)입니다. Claude Code 기반 자율 공격/방어 에이전트인 raptor에서 우리 자신도 슬롭 보고를 받았기 때문입니다. 재미있을 거예요!
코드 카나리아는 익숙한 선별 신호(예: 테스트 파일에서의 탐지, 예제 시크릿, 존재하지 않는 경로)를 의도적인 표식, 즉 유인책으로 확장한 것입니다. 테스트에서 이러한 카나리아는 슬롭을 식별하는 데 충분히 효과적이지만, 더 개선할 수 있습니다(실제 코드에 삽입하거나, 함수/파일/디렉터리 이름을 덜 암시적으로 만들거나, 새 코드처럼 정기적으로 재생성하는 등).
작성자: Gadi Evron (@gadievron), John Cartwright (@grokjc), Daniel Cuthbert (@danielcuthbert), Michal Kamensky (@kamenskymic, 프로젝트 이름을 지어준 공로를 인정합니다).
사용에 따른 책임은 본인에게 있습니다. 이 코드를 프로덕션에 붙여넣는다면 그것은 본인 책임입니다. 아래 면책 조항을 참조하세요.
들어오는 각 보고서에 대해 다음 순서로 처리합니다:
zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus; F+G를 비공개로 채택한 경우 handle_*_request도 포함)을 grep으로 찾으면 → 종결. (Rust는 Python과 동일한 zqx_tarnish_v3 스네이크 케이스 이름을 사용합니다. Go는 JS 이름과 동일한 zqxTarnishV3를 사용합니다.)CVE-2025-99919(가짜)을 grep으로 찾으면 → 종결.두 가지 유형의 카나리아:
| 단계 | 파일 | 형태 |
|---|---|---|
| A | python/legacy_utils.py, python/session_restore.py, python/compat_tokens.py, js/legacy_utils.js, rust/legacy_utils.rs, rust/session_restore.rs, go/legacy_utils.go, go/session_restore.go | 약 15개의 CWE 싱크 + 가짜 시크릿 + 시볼레스(shibboleth) |
| B | c/buffer_ops.c, rust/buffer_ops.rs, go/buffer_ops.go | 4가지 memcpy/memmove 형태 (CWE-120/121/787/170) |
| C | A로 병합됨 | 확장된 CWE 산출 |
| D | c/heartbeat.c + c/sat.h, c/tls_heartbeat.c, rust/heartbeat.rs, rust/tls_heartbeat.rs, go/heartbeat.go, go/tls_heartbeat.go | 하트블리드 실루엣 |
| E | python/regex_validator.py, js/regex_validator.js, rust/regex_validator.rs, go/regex_validator.go | 치명적 역추적(backtrack) 정규식 + 가짜 CVE-2025-99919 |
| F+G | private/fractal_dag/ (이 저장소에는 없음) | handle_*_request 항목들로 구성된 12-노드 DAG 전반의 A 단계 싱크 |
각 단계가 취약해 보임에도 불구하고 어떻게 비활성 상태를 유지하는지는 안전 모델을 참조하세요.
카나리아 코드 파일은 의도적으로 그럴듯한 더 이상 사용되지 않는(deprecated) 모듈처럼 읽히도록 작성되었습니다. 주석이나 식별자에 "canary", "honeypot", "tripwire" 같은 표현이 전혀 없습니다. 이렇게 해야 파일이 스캐너에게 스스로 정체를 드러내지 않지만, 그만큼 이 파일들이 안전한 이유에 대한 문서는 각 파일의 docstring 대신 여기에 존재합니다. 카나리아를 검토하거나 교체할 때 아래의 모든 계층이 그대로 유지되는지 확인하세요.
이러한 파일들은 취약한 형태의 코드입니다. 일부(예: python/session_restore.py의 pickle.loads, c/tls_heartbeat.c의 경계 없는 memcpy)는 도달 가능하다면 실제로 악용될 수 있습니다. 이것이 의도된 설계입니다. 스캐너가 싱크를 탐지하는 것이 핵심이며, 아래 계층들은 스캐너의 신호가 아니라 실행을 차단합니다. 패턴 기반 스캐너(grep, semgrep, LLM 기반 슬롭 파이프라인 — 주요 위협 모델)는 소스를 텍스트로 읽고 런타임 도달 가능성과 무관하게 결과를 표면화합니다. 빌드 통합형 C 스캐너(CodeQL 기본, clang-static-analyzer)는 컴파일된 파일만 보므로 빌드되지 않은 C 카나리아는 이들에게 보이지 않습니다. 슬롭 생성기가 압도적으로 빌드가 아닌 소스를 읽기 때문에 이는 수용 가능한 트레이드오프입니다.
다섯 개의 독립적인 계층이 이 파일들을 비활성 상태로 유지합니다:
raise ImportError / throw new Error — 일반적인 import / require는 어떤 정의가 바인딩되기 전에 중단됩니다.if False: / if (false) 아래에 위치 — 1번 계층이 우회되더라도 이름이 런타임 네임스페이스에 들어가지 않습니다.__all__: list[str] = [] (star-import는 아무것도 내보내지 않음). JS: module.exports = {} (CommonJS 사용자는 빈 객체를 받음).zqx_tarnish_v3, zqxTarnishV3, _validate_pep_440_plus). 이들 중 하나를 인용하는 보고서는 스스로 슬롭임을 드러냅니다.SECURITY.md.template 및 사용 방법 참조).E 단계는 여섯 번째 계층을 추가합니다: 치명적 역추적 정규식은 모듈 범위에서 re.compile / new RegExp에 전달되지 않고 문자열 리터럴로만 저장됩니다. 1번 계층을 제거하는 하네스(harness)조차 컴파일된 역추적 엔진을 트리거할 수 없습니다.
스캐너는 AST를 따라 raise / throw를 지나 데드 블록까지 탐색하므로 싱크는 여전히 발견 항목으로 표면화됩니다 — 이것이 의도된 동작입니다.
buffer_ops.c)안전성은 구조적입니다 — 각 형태에는 증명(proof)이 함께 있습니다:
bufops_copy_banner — src는 문자열 리터럴이고, n = sizeof(literal)이며, _Static_assert가 대상 크기에 고정합니다.bufops_copy_bounded — memcpy 바로 앞 줄의 if (n > dst_cap) n = dst_cap;이 CWE-787 주장을 불가능하게 만듭니다. C17 memcpy(dst, NULL, 0) UB를 피하기 위해 n == 0에서 단락(short-circuit)됩니다.bufops_copy_truncating — n <= dst_cap - 1, dst[n]은 최대 dst_cap - 1까지만 접근합니다. dst_cap == 0이면 조기 반환합니다.bufops_shift — i + n과 j + n 모두 cap으로 제한됩니다. memmove는 겹침(overlap)을 명시적으로 지원합니다.추가 격리: 모든 함수는 static(외부 링크 없음)이며 파일은 어떤 빌드 대상에도 추가되지 않습니다.
heartbeat.c + sat.h)하트블리드 실루엣(uint16_t payload_len → malloc(1+2+payload_len+16) → memcpy)은 계층화된 가드로 무력화됩니다:
sat_sub 사용 (래핑 없음).const 지역 변수에 캐시 — TOCTOU 및 시그널 핸들러 UB 창을 차단._Static_assert(SIZE_MAX - 19 >= UINT16_MAX, ...)는 malloc 크기가 size_t를 오버플로할 수 없음을 증명합니다.payload_len > 0 단락 검사는 빈 페이로드에서 memcpy(dst, NULL, 0) UB를 방지합니다.parse_heartbeat와 read_u16_be는 static이며 파일은 어떤 빌드 대상에도 링크되지 않습니다.인용된 줄의 특정 가드를 검토하지 않고 parse_heartbeat의 OOB 읽기/쓰기를 주장하는 보고서는 악용 가능성을 검증하지 않은 것입니다 — 선별 규칙 5번으로 종결하세요.
c/tls_heartbeat.c는 동일한 실루엣의 변형으로, 의도적으로 가드 없이 유지됩니다: process_heartbeat는 static이며 파일은 어떤 빌드 대상에도 링크되지 않습니다 — B 단계의 포괄 규칙과 마찬가지로 격리가 유일한 계층입니다. TU(번역 단위) 외부에서 호출하려 하면 링크 오류가 발생합니다.
다섯 개의 독립적인 계층이 이 파일들을 비활성 상태로 유지합니다 (Python/JS 안전 모델과 동일):
compile_error! — mod 또는 include!를 통해 크레이트에 파일을 포함하면 어떤 정의가 평가되기 전에 컴파일러 하드 오류가 발생합니다.#[cfg(any())] 뒤에 위치 — cfg(any())는 항상 false이므로 1번 계층이 우회되더라도 이름이 컴파일된 출력에 들어가지 않습니다.pub 항목 없음 — 위 두 계층이 모두 제거되어도 아무것도 외부로 내보내지지 않습니다.zqx_tarnish_v3). 이를 인용하는 보고서는 스스로 슬롭임을 드러냅니다.mod 문에도 참조되지 않고, 어떤 Cargo.toml에도 나열되지 않으며, 빌드 산출물에서 제외됩니다.E 단계는 여섯 번째 계층을 추가합니다: 치명적 역추적 정규식은 모듈 범위에서 regex::Regex::new에 전달되지 않고 &str 상수로만 저장됩니다.
buffer_ops.rs)안전성은 구조적이며 C 버전과 동일합니다. 각 형태에는 증명이 있습니다:
bufops_copy_banner — src는 b"status: ok\0"이고, 복사 길이는 BANNER.len()입니다. const 단언이 대상 크기에 고정합니다.bufops_copy_bounded — 복사 바로 앞 줄의 if n > dst_cap { n = dst_cap; }이 쓰기 범위를 제한합니다. n == 0에서 단락됩니다.bufops_copy_truncating — n <= dst_cap - 1, dst.add(n)의 NUL 쓰기는 최대 dst_cap - 1까지만 접근합니다. dst_cap == 0이면 조기 반환합니다.bufops_shift — i + n과 j + n 모두 cap으로 제한됩니다. ptr::copy는 겹침을 명시적으로 지원합니다.추가 격리: 모든 함수는 #[cfg(any())](데드 코드) 내부에 있으며 비공개이고, 파일은 어떤 빌드 대상에도 추가되지 않습니다.
heartbeat.rs + tls_heartbeat.rs)Rust의 하트블리드 실루엣은 unsafe 원시 포인터 연산(ptr::copy_nonoverlapping, std::alloc::alloc)을 사용하며, C 버전과 동일한 계층화된 가드로 무력화됩니다:
usize::saturating_sub를 통한 포화 뺄셈.const 단언(usize::MAX - 19 >= u16::MAX)은 할당이 오버플로할 수 없음을 증명합니다.payload_len > 0 단락 검사.#[cfg(any())] 내부에 있으며, 비공개이고, 파일은 링크되지 않습니다.