의존성 고정 vs 변동 버전 — 보안팀이 반드시 알아야 할 내용
출처: Dev.to
의존성 버전은 프로덕션 빌드가 매번 동일한 안전한 코드를 설치하도록 할지, 아니면 다른 릴리스를 조용히 가져올지 결정할 수 있습니다. lodash@4.17.21, lodash@^4.17.0, lodash@>=4.0.0는 각각 매우 다른 보안 결과를 만들 수 있습니다.
그래서 의존성 피닝 보안이 중요합니다. 피닝은 재현성을 향상시키고, 플로팅 범위는 업데이트를 허용하며, 잠금 파일은 양쪽 사이에 위치합니다. 올바른 전략은 팀이 오래된 취약한 버전에 얽매이지 않고 예상치 못한 패키지 변경을 피하도록 돕습니다.
의존성 피닝은 정확한 패키지 버전을 지정하는 것을 의미합니다. 예를 들어, lodash@4.17.21은 오직 4.17.21 버전만 허용합니다. 패키지 매니저는 개발자가 요구 사항을 변경하지 않는 한 4.17.20이나 4.17.22와 같은 다른 버전을 선택해서는 안 됩니다.
플로팅 버전은 정확한 버전이 아닌 범위를 사용합니다. 예를 들어, lodash@^4.17.0은 패키지 매니저 규칙에 따라 동일 메이저 릴리스 라인 내에서 호환되는 최신 버전을 허용할 수 있습니다. >=4.0.0과 같은 넓은 범위는 더욱 관용적이며 향후 많은 버전을 허용할 수 있습니다.
반 피닝은 일반적인 중간 경로입니다. manifest 파일은 범위를 사용하지만, 잠금 파일은 정확히 해제된 버전을 기록합니다. 예를 들어, package.json에는 “lodash”: “^4.17.0”이 포함될 수 있으며, package-lock.json은 프로젝트에 실제로 설치된 정확한 버전을 기록합니다.
피닝된 의존성: axios@1.7.7과 같은 정확한 버전은 재현성을 높이는 대신 수동 패치 이동이 필요합니다.
플로팅 의존성: axios@^1.7.0와 같이 버전 범위는 새로운 호환 가능한 릴리스를 허용합니다.
잠금 파일: package-lock.json, composer.lock 또는 poetry.lock과 같은 해제된 의존성 기록입니다.
전달 의존성: 다른 의존성이 요구하는 패키지로 설치되며, 프로젝트에 직접 나열되지 않은 경우에도 포함됩니다.
가장 안전한 의존성 전략은 일반적으로 “모든 것을 영구 피닝”하거나 “무조건 플로팅”하지 않습니다. 대부분의 팀은 재현 가능한 설치와 빠른 패치 자동화가 필요합니다.
피닝은 보안 팀에게 주요한 이점을 제공합니다: 확실성입니다. 애플리케이션이 express@4.18.2를 사용한다고 명시하고 빌드가 정확히 해당 버전을 설치한다면, 팀은 환경을 재현하고 동일한 코드를 테스트하며 신뢰감 있게 사고를 조사할 수 있습니다.
보안 대응 시 이 점이 중요합니다. CVE가 log4j-core 2.14.1에 영향을 미친다면, 팀은 해당 정확한 버전이 프로덕션에 존재하는지 확인해야 합니다. 정확한 피닝과 잠금 파일은 답변을 쉽게 만들어 줍니다. 그 외에, 답변은 설치 시점에 따라 달라질 수 있습니다.
피닝은 또한 예상치 못한 업데이트를 감소시킵니다. 패키지 유지 관리자는 깨진 릴리스를 발표하거나 기능을 제거하고 위험한 의존성을 추가하거나 우연히 취약점을 도입할 수 있습니다. 프로덕션 빌드가 자유롭게 플로팅한다면, 해당 릴리스는 코드 변경 없이도 여러분의 저장소에 진입할 수 있습니다.
또 다른 이점은 감사 가능성입니다. 버전이 고정되어 있으면 팀은 한 빌드와另一 빌드를 비교하여 정확히 어떤 의존성이 변경되었는지 볼 수 있습니다. 이를 통해 릴리스 검토, 사고 조사, 롤백, 규정 준수 증거를 지원합니다.
의존성 피닝 보안에서 가장 강력한 이점은 다음과 같습니다:
- 재현 가능한 빌드: 개발, CI, 스테이징, 프로덕션 모두 동일한 버전을 설치할 수 있습니다.
- 예상치 못한 패키지 이동 없음: 팀이 업데이트하지 않는 한 새로운 릴리스가 생산에 진입하지 않습니다.
- 명확한 감사 트레일: 버전 변경은 풀 리퀘스트와 잠금 파일 디프에서 눈에 띕니다.
- 보다 나은 사고 대응: 팀은 취약한 버전이 설치되었는지 확인할 수 있습니다.
- 무작위 고장 감소: 새로운 의존성 릴리스가 동작을 바꾼 경우 빌드가 실패하는 경우가 적습니다.
팁: 피닝은 정기적인 패치 검토와 결합될 때 가장 가치가 큽니다. 업데이트 없이 정확한 버전은 기술 부채가 됩니다.
피닝의 실제 단점은 취약한 코드를 고정시킬 수 있다는 것입니다. 프로젝트가 lodash@4.17.10을 피닝하면 자동으로 4.17.21로 이동하지 않습니다. 의존성 업데이트를 담당하는 사람이 없으면 피닝된 버전은 수개월 또는 수년이 지나도 취약한 상태를 유지할 수 있습니다.
이 문제는 “피닝 드리프트”라 불립니다. 피닝된 의존성이 변하지 않은 기간이 길어질수록 최신 릴리스에 뒤처지게 됩니다. 결국 업그레이드는 한 번에 많은 버전을 건너뛰어야 하므로 고통스럽습니다. 테스트가 실패하고 API가 변경되며 개발자는 작업을 다시 미뤄야 합니다.
플로팅 버전은 패키지 매니저가 최신 릴리스를 선택하도록 허용함으로써 이 문제를 일부 해결합니다. 패치 릴리스에는 보안 수정이 포함될 수 있으며, 범위는 매니페스트를 직접 편집하지 않고도 그 수정을 설치하도록 허용할 수 있습니다. 하지만 이 이점이 도움이 되는 것은 인스톨레이션이 제어되고 검토될 때만입니다.
플로팅의 위험한 형태는 프로덕션 빌드가 잠금 파일을 사용하지 않고 의존성을 해제할 때 발생합니다. 그 경우, 동일한 소스 코드에서 두 번의 빌드가 다른 패키지 버전을 설치할 수 있습니다. 이는 테스트를 약화시키고 공급망 위험을 초래합니다.
경고: 업데이트 자동화가 없으면 오래된 취약 패키지를 실행하게 될 수 있습니다. 잠금 파일 없이 플로팅하면 검토되지 않은 패키지 버전이 프로덕션에 진입할 수 있습니다.
잠금 파일은 대부분의 애플리케이션에 가장 적합한 균형을 제공합니다. manifest 파일은 합리적인 범위로 의도를 표현하고, 잠금 파일은 실제로 해제되고 테스트된 정확한 버전을 기록합니다.
npm에서는 manifest가 package.json이고, 잠금 파일은 package-lock.json입니다.
Yarn에서는 yarn.lock입니다.
PHP Composer에서는 composer.json과 composer.lock 파일이 있습니다.
Python 프로젝트에서는 Poetry와 Pipenv 같은 도구가 poetry.lock와 Pipfile.lock을 사용합니다.
Go는 모듈 요구 사항과 검증을 위해 go.mod와 go.sum를 사용합니다.
잠금 파일은 해제된 의존성 그래프를 보존함으로써 빌드를 재현 가능하게 합니다. manifest가 ^4.17.0이라고 해도 잠금 파일은 실제 설치된 버전이 4.17.21일 수 있습니다. CI와 프로덕션은 매번 새로운 버전을 해제하는 대신 해당 잠금 파일을 사용해 설치해야 합니다.
npm ci
npm에서는 npm ci가 잠금 파일에서 설치를 수행하고 깨끗한 CI 환경을 위해 설계되었습니다. 빌드 중에 예상치 못한 의존성 해제를 방지합니다.
composer install —no-dev —prefer-dist
Composer에서는 composer install이 composer.lock을 읽습니다. 의도적으로 최신 버전을 해제하고 잠금 파일을 업데이트하고자 할 때만 composer update를 사용합니다.
이 중간 경로는 프로덕션 설치가 정확한 상태를 유지하면서도 업데이트는 풀 리퀘스트를 통해 관리될 수 있게 지원합니다. 팀은 패키지가 이동하는 시점을 제어하고 병합 전 모든 변화를 테스트할 수 있습니다.
각 생태계는 의존성 해제를 다르게 처리합니다. 강력한 정책은 전 세계에 일괄적으로 적용하는 대신 그 차이를 존중해야 합니다.