주요 내용
- 패치된 빌드는 Maven과 Python에서는 업스트림보다 순위가 높지만, 에서는 그보다 낮습니다.
npm그리고 Gradle. - 4개의 패키지 관리자에서 수행된 8가지 제약 조건 검사 결과, 패치가 적용된 빌드가 통과한 경우는 단 3번에 불과했다.
- 리졸버의 가시성, 즉 리졸버가 별도의 요청 없이 패치된 빌드를 선택하는지 여부가 수정 사항이 실제로 배포될지 여부를 결정합니다.
- 어디서나 통하는 방법은 패치된 빌드를 명시적으로 고정하고, 배포될 빌드를 다시 빌드하는 것입니다.
다음은 우리가 한 번 이상 목격해 온 시나리오입니다. 어떤 팀이 업그레이드할 수 없는 라이브러리에 보안 수정 사항을 백포트하고, 패치된 빌드를 내부 레지스트리에 게시한 뒤 다음 작업으로 넘어갑니다. CI는 성공(그린) 상태입니다. 일주일 후, 스캐너는 여전히 해당 CVE를 감지하고, 누군가 마침내 잠금 파일을 열어보니 패치된 빌드가 없습니다. 실패한 것은 아무것도 없습니다. 패키지 관리자가 수정된 버전을 확인한 후, 해당 버전이 요건을 충족하지 않는다고 판단하고 대신 취약점이 있는 버전을 설치한 것뿐입니다.
그건 ~의 버그가 아닙니다. npm, 혹은 Gradle, 또는 그 밖의 어떤 것이든. 각 생태계는 그 버전에 대한 규칙이 정한 대로 정확히 작동할 뿐입니다. 문제는 이러한 규칙들이 릴리스를 배포하는 업스트림 유지보수자를 위해 작성된 것이지, 다른 사람의 릴리스 위에 수정 사항을 얹어 배포하는 사람을 위해 만들어진 것이 아니라는 점입니다. 여러 생태계에 걸쳐 패치된 오픈소스 패키지를 제공하는 것은 SecureChain에서 우리가 하는 일의 큰 부분을 차지하며, 우리가 배운 가장 유용한 교훈은 수정 사항에 부여하는 버전 번호가 수정 사항 자체만큼이나 중요하다는 사실입니다.
‘해결자의 가시성’이란 무엇인가
해결자 가시성이란, 프로젝트가 이미 선언한 버전 제약 조건이 주어졌을 때 패키지 관리자가 패치된 빌드를 스스로 선택할지 여부를 의미합니다. 해결자 가시성이 높은 빌드는 매니페스트를 수정할 필요 없이 기존 범위 및 핀에 의해 자동으로 선택됩니다. 반면, 해결자 가시성이 낮은 빌드는 누군가가 수동으로 적용할 때까지 레지스트리에 남아 완전히 사용 가능하면서도 완전히 무시됩니다.
가시성은 두 가지 요소에 따라 달라집니다. 첫 번째는 순서 문제로, 생태계가 패치된 버전을 그 기반이 되는 업스트림 릴리스보다 최신인지, 아니면 구버전인지 판단하는 것을 의미합니다. 두 번째는 일치 여부 문제로, 캐럿 범위나 정확한 핀과 같은 일반적인 제약 조건이 패치된 버전을 허용 가능한 것으로 간주하는지 여부를 의미합니다. 이 두 가지 질문에 대한 답은 생태계마다 다르기 때문에, 한 곳에서는 완벽하게 작동하던 전략이 다른 곳에서는 별다른 문제 없이 실패하는 경우가 있습니다.
한 번의 빌드, 네 가지 서로 다른 평가
이를 구체적으로 설명하기 위해, 업스트림 버전의 가상의 패키지 하나를 예로 들어보겠습니다. 4.7.3, 그리고 동일한 릴리스의 패치된 빌드를 추가하여, 각 생태계에서 사용하는 실제 버전 로직을 통해 두 버전을 모두 실행했습니다. 즉, npm~의 자체 semver 라이브러리(7.6.0), Maven 3.9.11의 버전 비교기, 로컬 저장소를 대상으로 종속성을 해결하는 Gradle 8.14, 그리고 Python의 packaging 라이브러리, 즉 pip 버전을 평가하는 데 사용됩니다. 패치된 빌드의 이름은 4.7.3-patched.2 파이썬을 제외한 모든 곳에서, 파이썬에서는 4.7.3+patched.2, 이는 파이썬 사양에서 이 작업에 대해 제공하는 방식이기 때문입니다. 매 실행마다 락파일이 방해되지 않는 상태에서 새로 해결되었습니다.
그런 다음 각 참가자에게 두 가지 질문을 던졌습니다. 정상적인 호환 범위가 설정된 상태에서 (^4.7.0 에 대한 npm, [4.7,4.8) Maven 및 Gradle의 경우, ~=4.7.0 (파이썬의 경우), 어떤 버전이 더 나을까요? 그리고 4.7.3 정확한 버전으로 선언되었는데, 그럼 어느 쪽이 승리하나요?
| 생태계 | 패치된 빌드 순위 | 호환 가능한 제품군 | Exact 4.7.3 선택 항목 |
|---|---|---|---|
| npm | 4.7.3 미만 | Upstream 4.7.3 | Upstream 4.7.3 |
| Maven | 4.7.3 이상 | 패치된 빌드 | Upstream 4.7.3 |
| Gradle | 4.7.3 미만 | Upstream 4.7.3 | Upstream 4.7.3 |
| 파이썬 (pip) | 4.7.3 이상 | 패치된 빌드 | 패치된 빌드 |
8건 중 3건이 성공했는데, 그중 2건은 Python 관련이었습니다. Maven과 Gradle은 동일한 저장소에서 동일한 POM 파일을 읽어왔음에도 불구하고, 어떤 버전이 더 최신인지에 대해 정반대의 결론을 내렸습니다. 이 사실이 놀랍게 느껴지신다면, 여러분만 그런 게 아닙니다. 우리 팀 내에서도 몇몇 사람들이 놀랐습니다.
npm이 보안 수정 사항을 베타 버전처럼 취급하는 이유
npm 이 버전은 시맨틱 버저닝(SemVer)을 따르며, SemVer에 따르면 패치 번호 뒤에 하이픈이 오면 프리릴리즈로 간주됩니다. 프리릴리즈는 해당 정식 릴리즈보다 아래에 배치되므로, 4.7.3-patched.2 ~보다 나이가 많은 것으로 간주된다 4.7.3. ~에 관해서는 npm 이와 관련하여, 귀하의 강화된 빌드는 취약점이 있는 버전의 릴리스 후보(RC)입니다.
일치시키면 상황이 더 나빠집니다. 기본적으로, npm 범위 일치 기능은 프리릴리즈 버전을 완전히 건너뛰므로, ^4.7.0 그리고 ~4.7.0 패치된 빌드는 절대 선택하지 않습니다. 다음과 같이 범위가 지정된 경우에도 마찬가지입니다. ^4.7.3-patched.0, 사전 출시 버전을 명시적으로 선택한 경우에도 여전히 기본 설정을 선호합니다 4.7.3 둘 다 사용할 수 있을 때는, 왜냐하면 4.7.3 는 “더 최신”입니다.
팀들에게 실제로 타격을 주는 경우는 그보다 더 은근한 편입니다. 예를 들어, 내부 레지스트리에 패치된 버전이 저장되어 있다고 가정해 봅시다. 4.7.3 빌드와 오래된 4.7.2, 하지만 업스트림은 제외 4.7.3. 아마 이렇게 예상하겠지만 ^4.7.0 패치에 착지하기 위해. 테스트 결과, 이는 다음과 같이 확인되었습니다. 4.7.2, 더 오래되었지만 여전히 취약한 버전이기 때문에, 4.7.2 시중에 나와 있는 비프리릴리즈 버전 중 가장 최신 버전이었습니다. 해결책은 바로 거기에 있었고, npm 그 위를 넘어갔다.
그래서 npm, 패치된 빌드의 이름을 명확히 지정해야 합니다. 만약 foo 직접 의존성이라면, 단순히 정확한 버전을 입력하기만 하면 됩니다. dependencies. 트리 아래쪽에 있는 복사본의 경우, npm 오버라이드는 루트에 설정된 도구입니다. package.json 애플리케이션의:
{
"overrides": {
"foo": "4.7.3-patched.2"
}
}
이로 인해 모든 사본이 foo 패치된 빌드의 트리 내 어디에서든, 전이성이 있든 없든, 그리고 npm ls foo 그게 잘 붙었는지 알려줄 거예요. 몇 가지 요령을 알아두면 도움이 됩니다. npm 루트 프로젝트의 오버라이드만 인정하므로, 라이브러리는 해당 라이브러리를 사용하는 프로젝트에 대해 오버라이드를 설정할 수 없습니다. 만약 foo 또한 직접 의존성이므로, dependencies 오버라이드와 정확히 일치해야 합니다 (the "$foo" 참조 양식을 통해 두 항목을 동기화하거나), 또는 npm 다음 오류가 발생하여 설치를 거부합니다. EOVERRIDE 오류. 그리고 오버라이드는 예전 베이스에 그대로 머물러 있습니다. 상류에서 함선이 4.7.4, 오버라이드가 적용되지 않은 프로젝트라면 캐럿 범위 내에서 기꺼이 해당 오버라이드로 이동할 것이며, 4.7.4 업스트림에서 수정 사항이 승인되지 않았다면 백포트는 제공되지 않습니다. 그렇기 때문에 우리는 업스트림에서 패치가 출시될 때마다 이를 계기로 삼아 새로운 기반 위에 수정 사항을 재구축합니다. 따라서 4.7.4-patched.1 누군가가 필요로 하기 전에 이미 존재한다. Yarn의 resolutions 그리고 pnpm‘s overrides 자신들만의 구문을 사용하여 같은 개념을 따릅니다.
Maven과 Gradle은 동일한 POM을 읽지만 결과가 일치하지 않습니다.
Maven의 버전 비교기는 다음과 같은 몇 가지 한정자를 인식합니다. alpha, beta, rc, 그리고 snapshot, 그리고 인식하지 못하는 항목은 일반 릴리스 뒤에 정렬됩니다. 따라서 Maven에서는, 4.7.3-patched.2 보다 최신이다 4.7.3, 그리고 다음과 같은 범위 [4.7,4.8) 그것을 골랐다. 대체로 좋은 소식이다.
문제는 Maven에서 범위 표기법을 사용하는 사람이 거의 없다는 점입니다. POM에 명시된 단순 버전은 ‘부차적인 요구 사항’에 불과하며, Maven은 어떤 버전이 가장 최신인지가 아니라 트리 루트와의 거리를 기준으로 충돌을 해결합니다. 따라서 가장 가까운 선언이 다음과 같다면 4.7.3, 그러면 다음과 같은 결과가 나옵니다. 4.7.3, 끝. Maven의 순서 지정 방식은 패치된 빌드에 친화적이지만, 애초에 해결 과정에서 순서 문제를 고려하는 경우는 거의 없습니다. 패치된 빌드를 포함시키려면, 다음에서 이를 선언해야 합니다. dependencyManagement 최상위 수준에서, 이는 전이 트리가 요청한 내용을 모두 재정의합니다.
Gradle은 순서 지정 방식이 정반대입니다. Gradle의 규칙에 따르면, 숫자가 아닌 부분이 하나 더 추가될수록 버전 번호가 낮아지며, patched 소수의 특별한 단어 중 하나가 아닌가요 (rc, snapshot, release, sp, 그리고 몇 가지 다른 것들)이 라인에서 각자 자리를 차지합니다. 그래서 4.7.3-patched.2 아래에 정렬 4.7.3. Gradle은 또한 그래프에서 가장 높은 버전을 선택하여 충돌을 해결하므로, 한 종속성이 패치된 빌드를 요구하고 다른 종속성이 4.7.3, 테스트 빌드가 업스트림으로 반영되었습니다 4.7.3. Gradle이 해당 버전을 더 최신 버전으로 간주했기 때문에 취약점이 있는 버전이 선택되었습니다. 수정된 버전은 다음과 같이 선언된 엄격한 버전입니다. strictly, 의존성 자체에서든 의존성 제약 조건 내부에서든, 다음과 같이 version { strictly '4.7.3-patched.2' }. 여기서는 단순한 제약 조건만으로는 충분하지 않습니다. Gradle은 이를 단지 최소값으로만 취급하기 때문이며, 4.7.3 Gradle의 관점에서 볼 때, 이 빌드는 이미 패치된 빌드보다 우선 순위가 높습니다.
적절한 구분 기호를 선택한다면, 파이썬은 제대로 처리해 줍니다.
파이썬의 버전 사양인 PEP 440에는 다른 언어에는 없는, 바로 이러한 상황을 위해 마련된 로컬 버전 라벨이 있습니다. 더하기 기호 뒤에 오는 모든 것은 로컬 라벨이며, 4.7.3+patched.2 위의 종류 4.7.3. 더 좋은 점은, 다음과 같은 공개 핀처럼 ==4.7.3 일치 시 로컬 레이블을 무시하므로, 이미 고정된 프로젝트는 4.7.3 수락할 것입니다 4.7.3+patched.2 그리고 이 방식을 선호합니다. 매니페스트를 변경할 필요는 없습니다. 테이블에서 두 답변 모두 패치된 빌드로 반환되는 행은 이것뿐입니다. 하지만 락파일도 여전히 고려 대상입니다. 락파일이나 해시 고정 요구사항 파일은 다시 생성할 때까지 기존 빌드를 유지하며, 이미 4.7.3 설치된 상태는 누군가가 실행할 때까지 그대로 유지됩니다 pip install --upgrade.
문제는 대신 ‘포스트 릴리스’를 사용한다는 점입니다. 4.7.3.post1 더 높은 순위에 속하지만, ==4.7.3 일치하지 않으므로, 정확한 업스트림 버전에 고정된 모든 프로젝트는 이를 자동으로 건너뜁니다. 패치된 Python 빌드를 배포하는 경우, 고정된 프로젝트에는 로컬 레이블이 전달됩니다. 릴리스 후 버전을 사용하는 것은 이미 업그레이드를 진행 중이던 사용자들에게만 수정 사항이 적용되도록 하는 방법입니다.
실용적인 측면에서 한 가지 문제가 있습니다. PyPI는 로컬 버전 라벨이 포함된 업로드를 허용하지 않기 때문에, 패치된 빌드는 다른 곳, 대개 비공개 인덱스에 저장해야 합니다. 해당 인덱스가 어떻게 구성되어 있는지는 보안 측면에서도 중요합니다. pip 어떤 지수를 다른 지수보다 우선시하지 않습니다. 단지 검색 대상으로 지정된 모든 곳에서 가장 적합한 결과를 선택하기만 할 뿐입니다. 따라서 pip 빌드와 업스트림 패키지 모두에 사용되는 단일 인덱스를 사용하는 것이, 의존성 혼란을 초래할 수 있는 추가 인덱스를 억지로 덧붙이는 것보다 더 안전합니다.
패치된 패키지를 배포하거나 사용하는 경우 이것이 의미하는 바는 다음과 같습니다.
이 모든 과정에서 우리가 얻은 교훈은, 어떤 리졸버도 스스로 해결책을 찾아줄 것이라고 기대해서는 안 된다는 점입니다. 파이썬이 가장 가까우긴 하지만, 그 경우에도 올바른 버전 구문을 선택해야만 합니다. 그 외의 모든 경우에는, 오버라이드이든, dependencyManagement 엔트리 또는 엄격한 버전을 사용해야 하며, 배포된 모든 버전은 핀이 실제로 포함되도록 재빌드해야 합니다. 해당 애플리케이션을 직접 관리하는 경우, 한 곳에서 한 번만 변경하면 됩니다. 하지만 다른 팀이 사용하는 패키지를 공개하고, 수정 사항이 트리 구조에서 세 단계 아래에 적용된다면, 최상위 아티팩트가 수정된 버전을 포함하게 될 때까지 그 위의 패키지들도 순서대로 재빌드해야 합니다. 하위 단계의 누구도 여러분을 대신해 오버라이드를 추가해 주지 않을 것이기 때문입니다. 단일 아티팩트를 배포하는 것보다 작업량이 더 많지만, 네 가지 생태계 모두에서 동일한 방식으로 작동하는 유일한 접근 방식입니다.
또 다른 교훈은 버전 문자열을 통해 보안 상태를 판단하는 것을 그만둬야 한다는 점입니다. 접미사는 리졸버에게 해당 빌드가 어디에 속하는지 알려줄 뿐입니다. 이 접미사는 해당 빌드가 실제로 어떤 CVE를 수정하는지에 대해 신뢰할 만한 정보를 제공하지 않으며, 많은 스캐너들이 이를 4.7.3-patched.2, 이를 다시 다음으로 매핑합니다. 4.7.3, 그리고 해당 릴리스와 관련된 알려진 모든 CVE를 표시합니다. 빌드와 함께 제공되는 VEX 문서와 같이 수정된 내용을 기계가 읽을 수 있는 형식으로 기록한 자료가 있어야 스캐너가 이를 실제로 읽어들이는 한, 스캐너가 차이를 구분할 수 있습니다.
이 모든 것이 딱히 특이한 것은 아닙니다. 이는 단순히 패키지 관리자의 내부에 자리 잡은 공급망 보안의 한 부분일 뿐이며, 많은 훌륭한 수정 사항들이 이곳에서 무산되곤 합니다. 업스트림에서 더 이상 지원하지 않는 릴리스 라인을 유지 관리하고 있다면—바로 이 부분이 TuxCare의 ‘Endless Lifecycle Support’가 개입하는 지점입니다—오후 한나절 정도 시간을 내어 자체 리졸버가 실제로 무엇을 설치하는지 확인해 볼 가치가 있습니다.
자주 묻는 질문
해결자 가시성이란 무엇인가요?
리졸버 가시성이란, 프로젝트가 이미 선언한 버전 제약 조건이 주어졌을 때 패키지 관리자가 패치된 빌드를 자체적으로 선택할지 여부를 의미합니다. 이는 생태계가 업스트림 릴리스에 대해 패치된 버전을 어떻게 순위를 매기는지와, 범위나 정확한 핀 지정과 같은 일반적인 제약 조건이 해당 버전과 일치하는지 여부에 따라 달라집니다.
npm은 왜 패치된 패키지 버전을 설치하지 않나요?
npm 하이픈이 접미사로 붙은 모든 버전을 다음과 같이 처리합니다. 4.7.3-patched.2, 프리릴리즈로. 프리릴리즈는 해당 릴리즈보다 아래에 정렬되며, 일반적인 캐럿 및 물결표 범위 지정에서는 건너뛰어지므로, npm 대신 업스트림 버전이나 이전 릴리스를 설치합니다.
npm에서 패치된 전이적 종속성을 강제로 적용하려면 어떻게 해야 하나요?
[추가] overrides 루트로의 진입 package.json 정확한 패치 버전을 명시하는 것입니다. npm 그런 다음 의존성 트리 내의 모든 패키지 사본(전이적 의존성 포함)에 대해 해당 버전을 사용합니다. 해당 패키지가 직접적인 의존성이기도 한 경우, dependencies 오버라이드와 일치해야 하거나, npm 다음으로 끝납니다. EOVERRIDE 오류. 라이브러리의 오버라이드에서 package.json 해당 라이브러리가 종속성으로 설치될 때는 무시됩니다.
Maven은 사용자 정의 버전 식별자를 릴리스 버전보다 최신 버전으로 간주하나요?
네. Maven은 인식되지 않는 한정자를 일반 릴리스 뒤에 배치하므로, 4.7.3-patched.2 보다 최신이다 4.7.3. 하지만 Maven은 일반적으로 버전이 아닌 종속성 트리 내의 거리에 따라 충돌을 해결하므로, 여전히 다음이 필요합니다. dependencyManagement 패치된 빌드가 선택되었는지 확인하십시오.
Gradle은 왜 패치된 빌드보다 업스트림 버전을 선택하는가?
Gradle은 다음과 같은 추가적인 비숫자 부분을 -patched.2 일반 릴리스보다 낮은 버전으로 간주되며, 충돌 해결 시 가장 높은 버전이 선택됩니다. 패치된 빌드를 다음과 같이 선언하십시오. strictly, 종속성 자체에서든 종속성 제약 조건 내부에서든, 이를 강제 적용하려면.
==4.7.3 핀을 지정하면 pip가 4.7.3+patched.2를 설치할까요?
네, 패치된 빌드가 인덱스에 포함되어 있다면 말이죠. pip 보시다시피, PEP 440에 따르면 공개 핀은 일치 여부를 확인할 때 로컬 버전 레이블을 무시하며, 로컬 버전이 공개 버전보다 우선적으로 정렬되므로 새로 설치할 경우 패치된 빌드가 선택됩니다. 이미 설치된 환경에서는 4.7.3 필요 사항 pip install --upgrade, 그리고 먼저 잠금 파일을 다시 생성해야 합니다.
