· Security· SBOM· CI/CD· Frontend

CVE Critical 딱지, 전부 고쳐야 할까

CI에 SBOM 보안 스캔이 도입된 후, 담당 프로젝트의 취약점을 분석하고 조치하며 나름의 작업 흐름을 만들어간 경험

갑자기 내 일이 됨

CI 파이프라인에 SBOM 기반 보안 스캔이 추가됐다. 인프라팀에서 도입한 건데, 취약점이 발견되면 조치 완료 전까지 배포가 막힌다. 처음엔 보안팀이 알아서 해주겠거니 했는데, 실제로는 각 팀이 자기 코드 취약점을 직접 분석하고 고쳐야 했다.

내가 맡은 프로젝트의 프론트엔드 취약점 리포트는 내 몫이 됐다. npm 패키지 관련이 대부분이라 프론트엔드 개발자가 봐야 한다는 논리였다. 그렇게 취약점 대응이 내 업무에 추가됐다.

처음엔 뭘 어떻게 해야 할지 몰랐다

스캔 결과를 받으면 이런 식으로 나온다.

CVE-2024-XXXXX: lodash < 4.17.21 (High)
CVE-2024-YYYYY: minimist < 1.2.6 (Critical)
CVE-2024-ZZZZZ: json5 < 2.2.2 (High)

Critical이라고 빨간 딱지가 붙어있으면 일단 긴장된다. 근데 이게 진짜 우리 서비스에 영향을 주는 건지, 그냥 빌드 도구에서만 쓰이는 건지 구분이 안 됐다. npm audit 돌리면 수십 개씩 뜨는데, 전부 다 고쳐야 하나?

여기서 한참 헤맸는데, 스캐너가 붙이는 심각도는 그 패키지가 최악의 조건에서 얼마나 위험한지를 말하는 것이지 우리 서비스에서 얼마나 위험한지를 말하는 게 아니다. CVSS 점수는 취약점 자체의 속성이고, 그게 우리 코드까지 도달하는지는 따로 봐야 한다.

빌드 시점에만 쓰이는 패키지의 RCE와 사용자 입력을 그대로 받는 런타임 패키지의 XSS는 둘 다 High여도 우리에게 주는 의미가 전혀 다르다. 이걸 구분하지 못하면 전부 고치느라 시간을 쓰거나, 반대로 다 무시하다가 진짜를 놓치게 된다.

나름대로 정리한 분석 순서

몇 번 삽질하면서 내 나름의 체크리스트를 만들었다.

1. 일단 어디서 쓰이는지 확인

npm ls minimist

이걸로 해당 패키지가 어떤 경로로 들어왔는지 본다. 직접 설치한 건지, 다른 패키지가 끌어온 건지에 따라 대응 방법이 달라진다.

2. 프로덕션에 포함되는지 확인

devDependency에만 있는 취약점은 상대적으로 덜 급하다. 프로덕션 번들에 안 들어가니까. 물론 무시하면 안 되지만 우선순위를 조정할 수 있다.

번들 분석기로 실제로 포함되는지 확인하기도 한다.

3. 실제로 취약한 기능을 쓰는지 확인

CVE 상세 내용을 보면 어떤 함수나 기능이 취약한지 나와있다. 우리 코드에서 그 부분을 안 쓰면 실제 리스크는 낮다.

눈으로만 찾으면 놓치는 게 생긴다. 취약한 API가 직접 호출되는 경우는 오히려 찾기 쉽고, 래퍼를 한두 겹 거쳐 들어가는 쪽이 문제다. 그래서 코드 패턴을 볼 때 semgrep 같은 정적 분석을 같이 돌린다. 이 함수가 어디서 불리는지를 문자열 검색이 아니라 구문 단위로 보기 위해서다.

4. 공격자 입력이 거기까지 닿는지 확인

앞의 셋이 쓰이느냐를 본다면 이건 닿느냐를 본다. 취약한 함수를 쓰고 있어도 인자가 전부 우리가 만든 상수라면 공격 경로가 없고, 반대로 사용자 입력이 검증 없이 흘러 들어가면 그때부터는 실제 위험이다.

이 단계가 제일 오래 걸리고 그만큼 건너뛰고 싶어진다. 그런데 앞의 세 단계는 고쳐야 한다는 결론으로 가는 길이고, 안 고쳐도 된다는 결론을 받쳐주는 건 이 단계뿐이다. 고치는 데는 근거가 필요 없지만 안 고치는 데는 필요하니까. 그래서 여기를 건너뛰면 결국 전부 고치게 된다.

우선순위가 아니라 영향 판정으로

앞의 급함·이번 주·나중에 분류에는 약점이 있다. 남에게 설명이 안 된다는 것이다. “이건 왜 안 고쳤나요”라는 질문에 “덜 급해 보여서요”로는 답이 되지 않는다. 게다가 기한이 없으니 나중에로 미뤄둔 것들이 쌓이기만 한다.

그래서 기준을 급함이 아니라 영향 여부로 바꾸고, 판정마다 기한을 붙였다.

영향 타입기준조치 기한
영향없음공격이 불가능하다 — 빌드 전용, 취약 API 미사용, 외부 입력 미도달, 선행 조건 미충족2주 이상
영향 제한적공격 경로는 있으나 조건이 까다롭거나 피해 범위가 좁다2주 이내
영향있음취약 코드를 쓰고 있고 공격자 입력이 도달 가능하다3일 이내

여기서 하나 더 바뀐 게, 영향없음에도 기한이 붙어 있다는 점이다. 안 고쳐도 된다는 뜻이 아니라 2주 이상 두고 봐도 된다는 뜻이고, 정기 업데이트 때 같이 올라간다. 판정이 면제처럼 쓰이기 시작하면 그때부터는 판정을 남발하게 된다.

반복되는 판단은 규칙으로 굳혔다

같은 판단이 반복되면 패턴이 보인다. 매번 처음부터 따지지 않으려고 적어뒀다.

  • webpack·babel 등 빌드 도구의 하위 의존성 — 대부분 영향없음
  • devDependencies의 취약점 — 대부분 영향없음
  • 서버사이드 전용 패키지 — SSR 환경이면 영향 가능. 프론트엔드라고 넘기면 안 된다
  • 프론트엔드 UI 라이브러리 — 사용자 입력이 직접 전달되는 경로인지 확인
  • HTTP 클라이언트 — 프로덕션 런타임에서 쓰면 영향 가능성이 높다

대부분이라고 쓴 건 예외가 있어서다. 빌드 도구라도 CI에서 외부 입력을 받는 구조라면 얘기가 달라진다. 이런 규칙은 판단을 대신해주는 게 아니라 어디서 출발할지를 알려주는 정도다.

조치하는 방법들

제일 깔끔한 건 버전 업데이트

npm update lodash

근데 메이저 버전이 바뀌면 Breaking Change가 있을 수 있어서 테스트를 꼼꼼히 해야 한다.

간접 의존성이면 overrides 사용

직접 설치한 패키지가 아니라 다른 패키지가 끌어오는 경우, 상위 패키지가 업데이트 안 해주면 답이 없다. 그럴 땐 overrides로 강제 지정.

{
  "overrides": {
    "minimist": "^1.2.6"
  }
}

업데이트가 안 되면 패키지 교체 검토

유지보수 안 되는 패키지면 아예 다른 걸로 바꾸는 게 나을 때도 있다.

당장 못 고치면 임시 완화

해당 기능을 비활성화하거나, 입력 검증을 강화하거나. 일단 급한 불 끄고 나중에 제대로 고친다.

판정보다 근거가 중요하다

조치하고 나면 티켓에 결과를 남긴다. “영향없음, 조치 완료” 한 줄로 끝낼 수도 있지만, 그러면 나중에 같은 패키지가 다시 올라왔을 때 그 댓글에서 건질 게 없다. 왜 영향이 없다고 봤는지가 빠져 있으면 처음부터 다시 분석해야 한다.

그래서 형식을 고정했다.

설명: 어떤 조건에서 어떤 공격이 가능한 취약점인가
영향타입: 영향없음 / 영향 제한적 / 영향있음
영향: 우리 프로젝트에서의 실질적 영향. 패키지 사용 위치와 판정 근거
조치: 구체적으로 무엇을 했는가
조치계획: 판정에 따른 기한

이 중에서 실제로 값을 하는 건 영향 칸이다. 어디서 쓰이고 왜 도달하지 않는지를 여기에 적는다. 이게 빠지면 판정은 근거 없는 주장이 된다.

영향없음이라고만 적힌 댓글은 시간이 지나면 아무도 신뢰하지 않는다. 쓴 사람도 마찬가지다. 보안 대응에서 남기는 기록은 조치했다는 증빙이라기보다 그때의 판단을 다시 따라갈 수 있게 하는 자료에 가깝다. 같은 패키지가 다시 올라왔을 때 처음부터 파지 않으려면 무엇을 봤는지가 남아 있어야 한다.

자주 겪는 귀찮은 상황들

False Positive: 스캔에서 취약하다고 하는데 실제로 해당 코드 경로를 안 쓰는 경우. 분석 결과 정리해서 보안팀에 예외 요청해야 한다.

패치가 아직 없는 경우: 취약점은 공개됐는데 수정 버전이 안 나온 경우. 기다리면서 모니터링하거나 임시 완화 조치.

의존성 충돌: A 올리면 B가 깨지고, B 고치면 C가 안 되고. 시간 잡고 의존성 전체를 정리해야 할 때가 있다.

핫픽스 배포해야 하는데 CI가 막힘: 긴급 배포 건인데 관련 없는 취약점 때문에 CI가 안 통과할 때 제일 난감하다.

결국 절차로 고정했다

여기까지 오고 나니 매번 하는 일이 같았다. 의존 경로를 보고, 사용 환경을 보고, 코드 패턴을 뒤지고, 공격 경로를 따지고, 세 판정 중 하나를 고르고, 정해진 형식으로 근거를 적는다. 순서가 고정된 절차인 셈이다.

순서가 고정됐다면 사람이 외우고 있을 이유가 없다. 그래서 이 흐름을 클로드 코드 스킬로 옮겼다. 티켓 하나를 주면 의존 경로를 추적하고 코드 패턴과 정적 분석을 돌린 뒤 판정과 근거를 뽑아온다. 나는 그 판정이 타당한지만 본다.

만들면서 둘로 나눴다. 티켓으로 내려온 취약점을 건별로 파고드는 것과 스캔 대기 중인 프로젝트를 통째로 훑는 것은 필요한 시점도, 나오는 결과물도 다르다. 앞쪽은 지금 막힌 배포를 뚫는 일이고 뒤쪽은 다음에 막히지 않게 미리 보는 일이다.

다만 아직 나 혼자 쓴다. 팀에 공유하면 좋겠다고 생각은 하지만, 혼자 쓰는 절차와 남이 읽고 따라 할 수 있는 절차는 요구되는 게 다르다. 판정 기준은 문서로 옮길 수 있어도 이 경우엔 예외라고 느끼는 감각까지 옮겨지지는 않는다.

남는 생각

보안 대응이 특별한 일이 아니라 그냥 일상적인 운영 업무가 되어가는 느낌이다. npm 생태계를 쓰는 이상 피할 수 없는 부분인 것 같다.