가장 잘 알려진 CDP 탐지 기법은 자동화 클라이언트가 Runtime.enable을 호출했을 때 실행되는 Error.stack getter를 이용하는 방식이었습니다. 이 기법은 2025년 5월의 V8 inspector 커밋 두 개가 Chrome 138에 반영되면서 더 이상 작동하지 않게 되었습니다. 그렇다고 CDP로 제어되는 브라우저가 보이지 않게 된 것은 아닙니다. 이후 연구자들은 활성화된 Runtime 도메인을 관찰하는 다른 방법을 찾아냈고, 상용 탐지 솔루션 대부분은 원래부터 이 한 가지 검사에 의존하지 않았습니다. 프레임워크 흔적, headless 및 실행 환경의 단서, 입력 이벤트의 속성, 행동 패턴과 네트워크 신호를 근거로 판단합니다.
CDP 탐지란 무엇이며 주요 프레임워크가 모두 노출되는 이유
CDP 탐지란 브라우저가 사람이 아니라 Chrome DevTools Protocol을 통해 제어되고 있다는 사실을 알아내는 것입니다. 이 프로토콜은 디버깅용으로 만들어졌고, 일부 디버거 기능이 페이지 JavaScript에서 관찰할 수 있는 흔적을 남기기 때문에 탐지가 가능합니다.
Chrome DevTools Protocol은 DevTools 창 자체가 사용하는 WebSocket API로, Runtime, Page, Network, Input 같은 도메인과 각 도메인의 명령 및 이벤트로 구성됩니다. Chromium 자동화는 대부분 이 프로토콜을 거칩니다.
- Puppeteer는 CDP로 Chrome을 제어합니다. Firefox에는 현재 WebDriver BiDi를 사용합니다.
- Playwright는 Chromium에는 CDP를, Firefox에는 Firefox 내부의 별도 프로토콜인 Juggler를 사용합니다.
- Selenium 4는 ChromeDriver와 WebDriver로 통신하고, ChromeDriver는 내부적으로 CDP를 통해 Chrome을 제어합니다. Selenium은 BiDi로 전환하는 동안 CDP 명령도 직접 노출합니다.
Firefox는 이미 CDP에서 손을 뗐습니다. Mozilla는 Firefox 129에서 CDP 구현을 지원 중단(deprecated)하고 Firefox 141에서 완전히 제거했습니다. 따라서 2026년의 "CDP 탐지"는 자동화된 Chromium 계열 브라우저를 탐지하는 일을 뜻합니다.
방어자들이 이 신호군에 끌린 이유는 DataDome이 2024년 6월 연구(자사 위협 연구 블로그 글의 재게재)에서 밝힌 그대로입니다. "브라우저가 CDP로 계측되고 있음을 탐지할 수 있느냐가 대부분의 최신 봇 프레임워크를 탐지하는 핵심이다." 새 headless Chrome이 일반 Chrome과 코드베이스를 공유하게 되면서 핑거프린트 불일치는 대부분 사라졌습니다. 반면 프로토콜 수준의 부작용이라면 Puppeteer, Playwright, Selenium은 물론 같은 기반 위에 만들어진 알려지지 않은 프레임워크까지 잡아낼 수 있습니다.
고전적인 Runtime.enable 검사의 작동 방식
이 검사는 Runtime 도메인이 활성화되면 V8 inspector가 클라이언트에 미리보기를 보내기 위해 모든 console 인자를 직렬화한다는 점을 악용했습니다. 객체를 직렬화하려면 속성을 읽어야 하고, getter가 걸린 속성을 읽으면 페이지 코드가 실행됩니다.
Castle이 문서화한 버전은 다음과 같습니다.
let wasAccessed = false;
const e = new window.Error();
Object.defineProperty(e, 'stack', {
configurable: false,
enumerable: false,
get: function () {
wasAccessed = true;
return '';
}
});
console.debug(e);
// wasAccessed === true => 무언가가 Runtime 도메인을 수신 중
단계별로 보면 다음과 같습니다.
- Puppeteer, Playwright, ChromeDriver는 스크립트를 어디서 실행할지 알기 위해
Runtime.executionContextCreated이벤트가 필요하므로Runtime.enable을 호출합니다. 프로토콜 문서는 이 명령을 "실행 컨텍스트 생성 보고"를 활성화하는 것으로 설명합니다. - 도메인이 활성화되면 모든
console.*호출이Runtime.consoleAPICalled이벤트를 만들고, V8은 이를 위해 각 인자의 미리보기를 생성합니다. Error의 경우 미리보기를 만들려면.name,.message와 함께.stack을 읽어야 했습니다. 페이지가.stack을 getter로 바꿔 두었기 때문에 getter가 실행되어 플래그가 바뀌었습니다.- 아무것도 연결되지 않은 일반 브라우저에서는 미리보기가 생성되지 않으므로 getter도 실행되지 않습니다.
JavaScript 몇 줄이면 충분했고 비용도 거의 없었으며, navigator.webdriver나 user agent를 어떻게 조작했든 Runtime.enable을 호출하는 프레임워크는 모두 잡아냈습니다. 누출 지점이 페이지가 아니라 프로토콜에 있었기 때문에 JavaScript 속성을 패치하는 스텔스 플러그인으로는 해결할 수 없었습니다. 그래서 등장한 대응책은 드라이버 수준이었습니다. rebrowser-patches와 Patchright는 Runtime.enable 호출을 피하며, DataDome은 2024년에 nodriver와 Selenium Driverless가 "Runtime.enable을 사용하지 않는 저수준 CDP 명령"을 쓴다고 언급했습니다.
2025년 V8에서 바뀐 점
V8은 2025년 5월 두 개의 커밋으로 이 부작용을 막았습니다. inspector가 객체와 에러를 미리볼 때 사용자 정의 getter를 더 이상 실행하지 않게 된 것입니다. 이 수정은 V8 13.8에 포함되었고, V8 13.8은 Chrome 138(2025년 6월 24일 stable 출시)에 탑재되었습니다.
61a90754(2025년 5월 7일), "[inspector] Prevent side effects during object preview". "runtime 도메인을 활성화할 때 사용자 코드를 실행하는 것"을 막습니다.e08e9734(2025년 5월 9일), "[inspector] Prevent side effects during error preview". 에러 설명 생성 방식을 바꿔 "부작용이 있는 getter를 실행하지 않도록" 합니다.
두 커밋 모두 Chromium 버그 415094795를 참조합니다. 첫 번째 커밋은 Microsoft의 Playwright 메인테이너인 Pavel Feldman이 작성했으므로, 이 변경은 스텔스 프로젝트가 아니라 자동화 도구 쪽에서 나온 것입니다. Castle의 Antoine Vastel은 2025년 8월 28일 "고전적인 CDP 봇 탐지 신호가 갑자기 작동을 멈춘 이유(그리고 아무도 눈치채지 못한 이유)"라는 글에서 이를 다루며, 미리보기 중 사용자 getter를 건너뛰는 새 getErrorProperty() 헬퍼를 지목했습니다. Chrome 138 이상에서는 위 코드가 수정되지 않은 Puppeteer에서도 false를 반환합니다.
Runtime.enable은 아직 탐지할 수 있을까?
아마 가능하겠지만 예전만큼 확실하지는 않습니다. 2026년 3월 독립 연구자 Sveba는 V8 미리보기 경로 분석을 공개하며 남아 있는 두 가지 경로를 제시했습니다. 하나는 GetOwnPropertyDescriptor가 실패할 때 2025년 5월의 가드가 놓치는 코드 경로로, 작성자는 "패치 이후 Chrome에서 악용 가능함을 … 검증했다"고 밝혔습니다. 다른 하나는 객체의 프로토타입 체인에 Proxy를 두는 방식입니다.
let detected = false;
const trap = new Proxy({}, {
ownKeys() { detected = true; return []; }
});
const obj = Object.create(trap);
console.groupEnd(obj); // 어떤 console 메서드든 동작
// detected === true => inspector가 미리보기를 생성 중
분석에 따르면 inspector는 값 자체가 Proxy인지만 확인하고 프로토타입은 확인하지 않으며, 모든 키를 즉시 수집합니다. 그래서 체인을 열거하는 과정에서 ownKeys 트랩이 실행됩니다. 작성자는 2026년 3월의 content_shell 빌드에서 이를 테스트했고 당시 패치되지 않은 상태라고 보고했습니다. 저희는 모든 stable Chrome 릴리스에서 확인하지는 않았으므로, 보편적인 사실이 아니라 보고된 내용으로 받아들이시기 바랍니다.
지금 방어자가 Runtime.enable 신호에 기댈 수 있는 정도는 두 가지 이유로 제한됩니다. 첫째, DevTools를 열어 둔 실제 사용자도 같은 코드 경로를 거치므로 이 신호는 "봇이다"가 아니라 "inspector가 연결되어 있다"는 뜻일 뿐입니다. 둘째, 가장 잡아낼 가치가 있는 프레임워크들은 이미 Runtime.enable을 피하므로, 이 신호에 걸리는 것은 주로 수정되지 않은 Puppeteer, Playwright, Selenium입니다. Castle의 글에서도 말하듯 업계는 약한 신호를 여러 개 겹쳐 쓰는 방향으로 옮겨 갔습니다.
2026년에도 통하는 신호
지금도 유효한 신호는 프레임워크가 페이지 안에서 하는 일, 브라우저의 빌드와 실행 방식, 입력이 들어오는 방식, 그리고 네트워크상에서 세션이 보이는 행동에서 나옵니다. CDP 부작용 하나를 숨긴다고 해서 이 중 어느 것도 사라지지 않습니다.
메인 월드에 남는 프레임워크 흔적
기본 상태의 프레임워크는 저마다 고유한 흔적을 남깁니다. rebrowser bot detector는 다음과 같은 항목을 꼽습니다.
__pwInitScripts: Playwright가 기본적으로 모든 페이지의 전역 스코프에 추가합니다.sourceURL누출: Puppeteer는 평가하는 스크립트에//# sourceURL=pptr:…태그를 붙이며, 이 값이 후킹된 함수 안에서 수집한 에러 스택에 드러날 수 있습니다.- 메인 월드 실행: 스크립트가 페이지 자체의 월드에서
document.querySelector를 호출하면, 해당 함수를 래핑해 둔 페이지는 그 호출을 감지합니다. exposeFunction/ 바인딩:page.exposeFunction()과 Playwright의window.__playwright__binding__은 페이지가 열거할 수 있는 전역 객체를 설치합니다. 내부적으로는 "검사 대상인 모든 컨텍스트의 전역 객체에" 함수를 추가하는Runtime.addBinding을 사용합니다.- 유틸리티 월드 이름: 프레임워크는 알아보기 쉬운 이름의 격리된 월드를 만들며, rebrowser-patches로 이 이름을 바꿀 수 있습니다.
Init script의 흔적
Page.addScriptToEvaluateOnNewDocument는 스크립트를 "모든 프레임이 생성될 때(프레임의 스크립트를 로드하기 전에)" 실행합니다. 스텔스 플러그인은 이 방식으로 사이트 코드가 실행되기 전에 navigator를 패치합니다. 프로토콜은 누출되지 않지만 패치는 누출됩니다. Camoufox README의 설명처럼, 주입된 JavaScript는 Object.getOwnPropertyDescriptor로, 함수의 toString()이 더 이상 [native code]를 반환하지 않는 점으로, 또는 window와 worker 사이에 값이 다른 점으로 잡아낼 수 있습니다.
Worker 컨텍스트
패치가 가장 자주 실패하는 곳이 worker입니다. 페이지에 적용한 init script와 속성 오버라이드는 dedicated, shared, service worker에 자동으로 적용되지 않습니다. window는 위조된 값을 보고하는데 worker는 실제 navigator.userAgent, hardwareConcurrency, WebGL 렌더러를 보고한다면 강력한 단서가 됩니다. CreepJS가 worker 스코프를 별도로 검사하는 것도 이 때문입니다.
Headless와 빌드의 단서
- Headless shell과 new headless. Chrome은 Chrome 112에서
--headless=new를 도입했고, Chrome 132부터 기존 모드는 별도의chrome-headless-shell바이너리에만 남아 있습니다. Playwright는 headless 모드에서 기본적으로 chromium headless shell을 사용하며, 이는 실제 Chrome과 다르게 동작합니다. - 빌드 정체성. "Chrome for Testing" user agent, 기본 뷰포트(Puppeteer는 800×600, Playwright는 1280×720), 켜진 채로 남은
navigator.webdriver는 모두 rebrowser detector가 꼽는 저비용 검사 항목입니다. - 코덱. Chromium 프로젝트에 따르면 오픈소스 Chromium 빌드에는 Google Chrome에 포함된 H.264, AAC 같은 독점 코덱이 없습니다. Chrome이라고 주장하면서 H.264에 대한
canPlayType을 통과하지 못하는 브라우저는 사실 Chromium입니다. - 권한과 미디어 장치. headless 및 컨테이너 환경에서는 알림 권한 상태, 비어 있는 미디어 장치 목록, 없는 음성 합성 보이스가 user agent의 주장과 맞지 않는 경우가 많습니다. CreepJS와 BrowserScan이 이를 검사합니다.
합성 입력
Input.dispatchMouseEvent로 보낸 입력은 신뢰된 이벤트(isTrusted가 true)를 만들지만, 실제 입력에는 없는 속성을 지니고 있었습니다. 가장 잘 알려진 것은 전체 화면이 아닌데도 screenX/screenY가 pageX/pageY와 같은 현상이었습니다. Chromium은 2025년 9월 15일, Chrome 142 브랜치 포인트 이전에 병합된 change 6917162("Fix screen coordinates to avoid automation detection")로 이를 수정했습니다. 이 문제를 우회하려고 만들어진 CDP-Patches 프로젝트는 이제 CDP 입력도 coalesced 이벤트를 발생시킨다고 밝히고 저장소를 아카이브했습니다. 최신 Chrome에서는 이 두 가지 검사는 거의 무의미해졌지만, 궤적, 타이밍, 클릭 위치 분석은 여전히 유효합니다.
행동과 네트워크
아래 신호는 CDP와 무관하며, 벤더가 점수를 매기는 요소의 대부분을 차지합니다. Castle은 클라이언트 측 행동 분석, 환경 "red pill", 프록시 및 네트워크 탐지, 조직적 활동에 대한 시계열 분석을 꼽습니다. 네트워크 측면에는 주장하는 브라우저와 일치해야 하는 TLS 핑거프린트와 HTTP/2 핑거프린트, 그리고 IP 평판이 포함됩니다. GPU 수준의 핑거프린팅은 canvas 및 WebGL 심층 분석에서 다룹니다.
Patchright, nodriver, Camoufox의 CDP 누출 대응 방식
세 도구 모두 각자의 방식으로 Runtime.enable 계열 누출을 피하지만, 어느 것도 행동 패턴이나 네트워크 평판 문제는 해결하지 못합니다.
Patchright는 Chromium 전용으로 패치된 Playwright 드라이버입니다. README에 따르면 핵심 패치는 "(격리된) ExecutionContext에서 JavaScript를 실행하는 방식으로" Runtime.enable을 피합니다. 또한 Console 도메인을 완전히 비활성화하고(그래서 console 로깅이 작동하지 않습니다), --enable-automation을 제거하고, --disable-blink-features=AutomationControlled를 추가하며, 핑거프린트 주입 없이 실제 Chrome(channel="chrome")을 실행하도록 권장합니다. Playwright 릴리스를 바짝 따라가며(v1.63.0 드라이버가 2026년 9월에 출시), 설정 방법은 Patchright 심층 분석에서 다룹니다.
undetected-chromedriver의 후속작인 nodriver는 WebDriver를 없애고 설치된 Chrome과 CDP로 직접 통신합니다. 현재 core/ 소스를 살펴보면 Runtime.evaluate와 Runtime.callFunctionOn으로 평가를 수행하고 Runtime.enable은 한 번도 호출하지 않는데, 이는 DataDome의 2024년 설명과 일치합니다. 대신 평가가 메인 월드에서 이루어지므로 DOM 함수를 래핑하는 페이지에는 드러나고, 핑거프린트 관리 기능도 없습니다. 마지막 PyPI 릴리스는 2026년 5월의 0.50.3입니다. nodriver 가이드를 참고하세요.
Camoufox는 CDP를 아예 우회합니다. Firefox 포크이고, Playwright는 Juggler를 통해 Firefox를 제어하는데, Camoufox는 Juggler를 패치해 Playwright가 페이지의 격리된 사본에서 작업하도록 만듭니다. 따라서 페이지 스크립트는 주입된 헬퍼를 볼 수 없고 getter 읽기를 감시할 수도 없습니다. 입력은 Firefox의 일반 입력 핸들러를 거치고, 핑거프린트 값은 init script가 아니라 C++ 수준에서 위조되므로 toString 및 worker 불일치 계열의 문제가 사라집니다. 대가는 Firefox라는 점입니다. README에 따르면 일부 WAF는 SpiderMonkey 엔진의 동작을 검사하므로 Chrome으로 위장할 수는 없습니다.
신호 참고표
| 신호 | 2026년에도 유효한가? | 무력화 방법 | 무력화 비용 |
|---|---|---|---|
console을 이용한 Error.stack getter(고전적 방식) | Chrome 138+(V8 13.8)에서는 무효 | 최신 Chrome | 없음 |
| 기타 inspector 미리보기 부작용(예: 프로토타입 체인 Proxy) | 2026년 3월 기준 작동한다고 보고됨. DevTools를 연 실제 사용자에게도 발생 | Runtime.enable 미호출(Patchright, nodriver, rebrowser-patches) 또는 CDP 미사용(Camoufox) | 낮음~중간: 드라이버 패치, console API 상실 |
__pwInitScripts, 바인딩, pptr: sourceURL | 기본 상태 프레임워크에는 유효 | 패치된 드라이버, exposeFunction 회피 | 낮음 |
| 메인 월드 실행 후킹 | 유효 | 격리된 월드에서 평가 | 중간: 페이지 전역 객체에 직접 접근 불가 |
JS로 패치한 속성(toString, descriptor) | 유효 | 패치하지 않기. 실제 Chrome 또는 C++ 수준 위조 사용 | 중간~높음 |
| window와 worker 불일치 | 유효 | 위와 동일. 패치가 worker까지 적용되어야 함 | JS로 패치하면 높음 |
| Headless shell / Chrome for Testing / 코덱 | 유효 | 정식 Chrome, headful 또는 new headless, 가상 디스플레이 | 중간: RAM 증가, 서버에 Xvfb 필요 |
| CDP 입력: screenX == pageX, coalesced 이벤트 없음 | 최신 Chrome에서 대부분 수정됨 | 최신 Chrome | 없음 |
| 행동과 타이밍 | 유효하며 점수에서 차지하는 비중이 커지는 중 | 현실적인 요청 간격, ID당 요청 수 축소 | 높음: 처리량 감소 |
| TLS / HTTP/2 / IP 평판 | 유효 | 실제 브라우저 네트워크 스택, 깨끗한 레지덴셜 또는 ISP IP | 지속적인 프록시 비용 |
내 환경을 직접 테스트하는 방법
먼저 직접 만든 프로브 페이지로 테스트한 다음 공개 탐지 페이지로 테스트하고, 한 번에 변수 하나씩만 바꾸세요. 공개 페이지는 그 페이지가 무엇을 탐지하는지 알려 줍니다. 직접 만든 페이지는 어떤 Chrome 빌드와 드라이버 조합이 아직 어떤 검사에 걸리는지 알려 줍니다.
1. 로컬 프로브 페이지
아래 내용을 probe.html로 저장하세요. 고전적 검사, 프로토타입 체인 검사, 몇 가지 저비용 흔적 검사를 실행한 뒤 결과를 DOM에 기록하므로, 스크립트가 어느 월드에서 실행되든 어떤 드라이버로도 결과를 읽을 수 있습니다.
<!doctype html>
<pre id="out">running</pre>
<script>
function classic() {
let hit = false;
const e = new Error();
Object.defineProperty(e, 'stack', { configurable: false, enumerable: false,
get() { hit = true; return ''; } });
console.debug(e);
return hit;
}
function protoProxy() {
let hit = false;
const trap = new Proxy({}, { ownKeys() { hit = true; return []; } });
console.groupEnd(Object.create(trap));
return hit;
}
const r = {
classicStackGetter: classic(),
protoProxyPreview: protoProxy(),
webdriver: navigator.webdriver,
pwInitScripts: '__pwInitScripts' in window,
h264: document.createElement('video').canPlayType('video/mp4; codecs="avc1.42E01E"'),
ua: navigator.userAgent,
};
setTimeout(() => { document.getElementById('out').textContent = JSON.stringify(r, null, 2); }, 300);
</script>
그런 다음 확인하려는 각 환경에서 실행합니다.
import pathlib
from playwright.sync_api import sync_playwright # 다음으로 교체 가능: from patchright.sync_api import sync_playwright
url = pathlib.Path("probe.html").resolve().as_uri()
with sync_playwright() as p:
browser = p.chromium.launch(channel="chrome", headless=False)
page = browser.new_page()
page.goto(url)
page.wait_for_function("document.getElementById('out').textContent !== 'running'")
print(page.text_content("#out"))
browser.close()
기본 Playwright, 이어서 Patchright로 실행한 다음, 일반 Chrome 창에서 DevTools를 연 상태와 닫은 상태로 같은 페이지를 열어 보세요. DevTools 실행은 대조군입니다. 여기서 걸리는 항목은 실제 사용자에게도 걸리므로, 그 자체로는 약한 신호입니다.
2. 공개 탐지 페이지
- rebrowser bot detector: Chromium 중심이며 Runtime.enable, sourceURL, 메인 월드 실행,
exposeFunction,__pwInitScripts, 뷰포트, user agent를 다룹니다. 페이지 설명대로 일부 테스트는 스크립트가 페이지의dummyFn을 호출하거나 함수를 노출할 때만 작동합니다. - BrowserScan 봇 탐지: WebDriver, user agent, CDP, navigator 일관성 검사 결과를 보여 줍니다.
- CreepJS: "거짓말"(패치된 속성), headless 특성, worker 스코프 불일치를 표시하는 오픈소스 핑거프린트 분석 도구입니다. JavaScript 수준의 위조를 잡아내는 데 가장 좋습니다.
- Device and Browser Info "Are you a bot?": CDP, WebDriver, Playwright, headless Chrome 검사를 포함한 핑거프린트 및 행동 테스트입니다.
이 페이지들은 실제 운영 환경에서 쓸 프록시를 통해 실행하세요. 여러 페이지가 브라우저 신호와 함께 IP와 시간대도 보여 주며, 데이터센터 IP에서 받은 완벽한 브라우저 점수는 실제 트래픽에 대해 알려 주는 바가 거의 없습니다. sticky 세션(레지덴셜 게이트웨이에서 USERNAME-sid-abc123-ttl-10m)을 사용하면 실행 결과를 비교하는 동안 IP가 고정됩니다.
테스트 페이지를 통과하는 것과 실제 사이트를 통과하는 것은 다르다는 점을 기억하세요. 상용 벤더는 서버 측 모델, 세션 간 이력, 그리고 이런 페이지가 볼 수 없는 신호를 사용합니다.
이것으로 해결되지 않는 것
CDP 누출을 막으면 증거 한 범주가 사라질 뿐입니다. 자동화가 사람처럼 보이게 되는 것도 아니고, 수집이 허용되는 것도 아닙니다. ID당 요청 속도, 순서, 양은 여전히 자동화를 드러내며, IP 평판과 TLS도 여전히 주장하는 브라우저와 일치해야 합니다. 접근 규칙도 그대로 적용됩니다. robots.txt, 서비스 약관, 그리고 개인 정보에 관해서는 개인정보 보호법을 지켜야 합니다. 탐지를 운영하는 입장이라면 2025년의 실질적인 교훈은, 브라우저 벤더가 일상적인 커밋 하나로 없앨 수 있는 단일 부작용에 의존하지 말라는 것입니다. 자동화를 운영하는 입장이라면 도구를 최신 상태로 유지하고, JavaScript를 패치하지 않는 드라이버를 우선하며, 남은 신호는 행동에 관한 것이라고 가정하세요.






