프로그레시브 JPEG, 다들 보신 적 있어요
웹에서 이미지가 로딩될 때 처음엔 흐릿한 전체 윤곽이 보이다가 점점 선명해지는 걸 본 적 있으실 텐데요. 이게 바로 '프로그레시브 JPEG'이에요. 일반 JPEG은 이미지를 위에서 아래로 차례대로 저장하지만, 프로그레시브 JPEG은 저해상도 밑그림을 먼저 저장하고 그 뒤에 디테일을 채워 넣는 데이터를 이어 붙이거든요. 덕분에 브라우저는 파일이 다 내려오기 전에도 대략적인 미리보기를 보여줄 수 있는 거죠.
그런데 한 개발자가 엉뚱한 질문을 던졌어요. '이 순서를 거꾸로 뒤집으면 어떻게 될까?' 세밀한 디테일을 파일 앞부분에 두고, 거친 밑그림을 맨 뒤에 두면요. 결과는 처참했대요. 대부분의 디코더는 깨진 화면을 보여주고, 어떤 건 아무것도 못 그리고, 몇몇은 아예 크래시가 났다고 해요. 그런데 여기서 멈추지 않고 한발 더 나아간 게 이 글의 재미있는 지점이에요.
스캔 순서는 사실 인코더 마음대로거든요
프로그레시브 JPEG의 각 '스캔'(데이터 조각)은 어떤 주파수 대역을 담을지, 정밀도 비트를 얼마나 담을지 같은 파라미터로 정의되는데요. 여기서 주파수 대역이 뭐냐면, JPEG은 이미지를 DCT라는 변환으로 '큰 덩어리 정보'와 '자잘한 디테일 정보'로 쪼개서 저장하거든요. 표준 인코더는 이걸 상식적인 순서로 내보내지만, 사실 JPEG 스펙은 그 순서를 강제하지 않아요. 그래서 글쓴이는 아예 자기만의 인코더를 C로 직접 만들어서, 일부러 괴상한 순서로 스캔을 내보내는 JPEG들을 만들었어요.
첫 번째 작품은 마지막 1KB가 도착하기 전까지는 TV 지직거림 같은 노이즈만 보이다가, 끝부분이 도착하는 순간 진짜 이미지가 '짠' 하고 나타나는 JPEG이에요. 대역폭을 아끼려고 이미지 앞부분만 받아서 처리하는 크롤러는 노이즈만 보고, 파일을 끝까지 받는 사람은 진짜 이미지를 보게 되는 거죠.
두 번째는 더 교묘해요. 50% 다운로드 시점과 100% 시점에 완전히 다른 그림이 보이는 JPEG인데요. 앞부분 스캔에는 미끼 이미지를 넣고, 뒷부분 스캔에서 점진적 정밀도 보정(successive approximation) 비트를 이용해 앞의 계수들을 상쇄시키고 전혀 다른 이미지로 바꿔치기하는 원리거든요. 보는 각도에 따라 그림이 바뀌는 렌티큘러 카드의 JPEG 버전인 셈이에요.
세 번째는 영원히 끝나지 않는 JPEG이에요. 파일 끝에 '데이터가 더 온다'고 약속하는 스캔 헤더를 붙여놓으면, 브라우저는 이미지를 잘 그리는데 파일 종료 마커(EOI)를 기다리는 lazy loading 라이브러리나 진행률 표시는 영원히 멈춰버려요.
실전 투입: 크롤러에게는 노이즈, 사람에게는 감자
글쓴이는 이걸 실제로 자기 사이트의 AI 크롤러 방어에 쓰고 있어요. 텍스트 쪽은 마르코프 체인으로 만든 무의미한 문장을 무한히 뿜는 타르핏(크롤러를 가두는 함정)으로 대응하고, 이미지 URL에는 이 '역행 JPEG'을 서빙하는 거예요. 미끼 레이어는 절차적으로 생성한 노이즈고, 전체를 다 받으면 감자 사진이 나온다고 해요. 앞부분만 받아 썸네일을 만드는 스크레이퍼는 자기가 노이즈 더미를 수집하고 있다는 것도 모르는 거죠. 게다가 1024x768 이미지 하나 생성하는 데 약 2ms밖에 안 걸려서, 진짜 이미지를 서빙하는 것보다 CPU가 덜 든대요. 의존성 없는 C 파일 하나로요.
디코더 호환성이 엉망이라는 점도 흥미로운데요. libjpeg-turbo는 잘 처리하고, Safari는 미끼 이미지를, Chrome은 최종 이미지를 보여주고, 몇몇 썸네일 라이브러리는 크래시가 난대요. 스펙을 벗어나지 않은 파일인데도 이렇게 제각각이라는 건, 실전 파서들이 '상식적인 입력'만 가정하고 만들어졌다는 뜻이겠죠.
우리에게 주는 시사점
robots.txt를 무시하는 AI 크롤러와의 싸움은 이제 하나의 장르가 됐어요. Cloudflare의 AI Labyrinth, 크롤러를 무한 미로에 가두는 Nepenthes, 작업증명을 요구하는 Anubis 같은 도구가 계속 나오는데, 이 프로젝트는 '파일 포맷 자체'를 무기로 쓴다는 점이 신선하거든요. 반대로 방어 체크리스트로도 읽을 수 있어요. 이미지 업로드를 받는 서비스라면, 우리 썸네일 파이프라인이 이런 '스펙에는 맞지만 괴상한' JPEG에 크래시하지 않는지 점검해볼 만해요. 파서를 만들 때 '설마 이런 입력이 오겠어'라는 가정이 얼마나 위험한지 보여주는 좋은 교재이기도 하고요.
한줄 정리: JPEG 스펙의 유연함을 역이용하면, 사람에게는 멀쩡하고 크롤러에게는 쓰레기인 이미지를 아주 싼 비용으로 만들 수 있다. 여러분이라면 AI 크롤러에 어떻게 대응하시겠어요? 차단파, 함정파, 아니면 그냥 두는 파?
🔗 출처: Hacker News