한 줄로
CSP(Content Security Policy, 페이지가 어떤 코드를 실행해도 되는지 브라우저에 알려주는 규칙)는 북마클릿을 막지 않는다. 막히는 건 그 북마클릿이 DOM 에 <script> 를 남기는 순간이다. 이 글은 그 경계를 직접 돌려 확인하는 과정에서 내 가설 두 개가 반증된 기록이다.
북마클릿이란?
북마크의 URL 칸에 javascript: 로 시작하는 코드를 넣어둔 것이다. 누르면 그 코드가 지금 보고 있는 페이지 위에서 실행된다. 확장 프로그램을 깔지 않고 페이지를 조금 바꾸거나 값을 꺼내 오고 싶을 때 쓴다.
javascript:document.body.style.zoom='1.5';void 0;
주소 칸에 넣는 것이라 URL 규칙을 그대로 받는다. 그래서 인코딩과 길이가 실무의 전부라고 봐도 된다.
자주 도는 주장 3개를 직접 돌려봤다
일반 설명글은 검색 결과와 다를 게 없어서, 흔히 보이는 주장들을 CSP 가 엄격한 페이지(GitHub, default-src 'none')에서 직접 실행해 근거를 깔기로 했다. 세 번의 PR 로 정정 발행했고, 그 과정에서 두 번 내 판단이 틀렸다.
1차 — “CSP 때문에 안 된다”는 절반만 맞다
PR #78 → fd05b35
default-src 'none' 인 GitHub 에서도 북마클릿 본문의 인라인 코드는 동작한다. 사용자가 직접 실행한 코드로 취급되기 때문이다. 막히는 것은 <script src=…> 로 외부 파일을 불러오는 쪽이었다.
결론은 작법으로 이어진다 — 의존성 없이 한 덩어리로 쓴다. 라이브러리를 불러다 쓰려는 순간 CSP 가 엄격한 사이트에서 죽는다.
클립보드도 확인했다. HTTPS 인 GitHub 에서 navigator.clipboard 가 NotAllowedError 를 냈다. secure context(HTTPS 같은 안전한 실행 맥락) 문제가 아니라 사용자 제스처 문제였다. 예전에 쓴 clipboard는-https에서만-동작한다 와 증상은 같지만 원인이 달라서, 같은 설명을 두 번 쓰는 대신 링크로 연결했다.
인코딩 실측:
- 예제 한 줄 113자 (처음 121자로 적었다가 재측정 후 정정)
- 한글 27자를 전부 인코딩하면 87자
-
#를 인코딩하지 않으면 뒤쪽 코드가 통째로 잘린다
2차 — “네트워크를 타면 걸린다”는 틀렸다
PR #79 → 2b512cc
1차 결과를 “외부에서 받아오면 걸리는구나” 로 일반화하려다 반례를 찾았다.
| 실행 경로 | 네트워크 | 결과 |
|---|---|---|
| 북마클릿 본문 | 안 탐 | 된다 |
<script> 만들어 코드 직접 넣기 |
안 탐 | 차단 |
<script src=…> 외부 로드 |
탐 | 차단 |
두 번째 줄이 가설을 깬다. 네트워크를 전혀 타지 않는데도 차단된다.
진짜 경계는 “DOM 에 남기느냐”다. <script> 엘리먼트를 문서에 꽂는 순간 그 코드는 내가 실행한 코드가 아니라 페이지가 실행하는 코드가 되고, 그때부터 CSP 의 검사 대상이 된다. 북마클릿 본문은 그 경계 바깥에 있어서 통과했던 것이다. 1차에 써둔 “외부 스크립트만 차단” 서술도 같이 고쳤다.
이 절 끝에는 단서를 달았다. CSP 가 지키려는 것은 북마클릿에서도 유효하며, 이건 규칙의 대상을 설명하는 것이지 회피법이 아니다.
3차 — “setTimeout 뒤로 넘어가면 실패한다”도 틀렸다
PR #80 → e9de581
클립보드가 시간이 지나면 권한을 잃는다는 가설은 진짜 클릭으로 재측정해 반증했다.
| 실행 시점 | 결과 |
|---|---|
| 클릭 직후 | ok |
| setTimeout 1.5초 뒤 | ok |
| 아무것도 안 누름 | NotAllowedError |
기준은 경과 시간이 아니라 “사람이 누른 흐름에서 출발했는가” 였다. 처음 테스트가 틀렸던 이유는 btn.click() 으로 재현했기 때문이다. 코드가 만든 합성 클릭은 신뢰되지 않은 제스처라 두 경우 모두 실패했고, 나는 그걸 시간 문제로 읽었다.
같은 PR 에서 내 예제가 깨져 있는 것도 발견했다.
javascript:document.body.style.zoom='150%';void 0;
이 문자열에는 %15가 없다. 끝의 % 뒤에 16진수 두 자리가 오지 않아 decodeURI가 URIError를 던진다. 이 글 자신이 경고하는 문제에 스스로 걸린 셈이다. zoom='1.5' 로 교체하고, 인코딩 절에 두 증상의 차이를 추가했다 — % 는 URL 자체가 깨지고, # 는 조용히 뒤만 잘린다. 두 문자는 오류가 드러나는 방식이 다르다.
맨 위 요약표가 교정 전 상태로 남아 본문과 모순되던 것도 이때 고쳤다. #79 에서 인라인 <script> 도 차단으로 바꿔놓고 표는 2개 케이스만 남겨뒀었다.
마지막으로, void 0 의 유래와 URL 길이 상한은 측정하지 않았음을 글에 명시했다. 실측치와 섞이면 나머지 수치까지 같이 의심받는다.
정리
- 북마클릿 본문의 인라인 코드는
default-src 'none'에서도 실행된다 -
<script>를 DOM 에 꽂으면 — 외부 로드든 코드 직접 삽입이든 — 차단된다 - 클립보드는 HTTPS 여부가 아니라 신뢰된 사용자 제스처에서 출발했는지를 본다
-
%와#는 인코딩하지 않으면 각각 URL 붕괴와 조용한 절단을 만든다
2·3차 검증에서는 같은 방식으로 가설이 틀렸다. 그럴듯한 일반화(네트워크, 경과 시간)를 먼저 세우고 실측이 그걸 깼다. 테스트 코드가 만든 합성 클릭을 진짜 클릭으로 착각한 3차가 특히 그렇다 — 측정 도구가 측정 대상의 조건을 바꿔놓고 있었다.