Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 708 – 의존성 혼동 공격을 완화하기 위한 저장소 API 확장

Author:
Donald Stufft <donald at stufft.io>
PEP-Delegate:
Paul Moore <p.f.moore at gmail.com>
Discussions-To:
Discourse thread
Status:
Rejected
Type:
Standards Track
Topic:
Packaging
Created:
20-Feb-2023
Post-History:
01-Feb-2023, 23-Feb-2023, 02-Apr-2026
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

거부

잠정 승인 상태로 3년을 보낸 후, 필요한 승인 조건이 충족되지 않았기 때문에 이 PEP는 거부되었습니다.

잠정 승인

이 PEP는 잠정 승인되었습니다. PEP가 최종 상태가 되기 전에 다음의 필수 조건을 충족해야 합니다:

  1. 프로젝트 소유자가 추적 데이터를 설정할 수 있도록 필요한 사용자 인터페이스 요소를 포함한 PyPI(Warehouse)의 PEP 구현입니다.
  2. PyPI 이외의 하나 이상의 저장소에서 PEP를 구현해야 합니다. 최소 두 개의 색인이 없으면 색인 병합을 실제로 테스트할 수 없기 때문입니다.
  3. 의도된 의미론을 지원하고 기대한 보안 이점이 달성되었음을 입증하는 데 사용할 수 있는 pip의 PEP 구현입니다. 이 구현은 처음에는 “기본적으로 비활성화”되어야 하며, 이는 사용자가 테스트에 직접 참여해야 한다는 의미입니다. 단순히 “소식이 없으면 좋은 소식”이라고 가정하기보다는, 새 기능을 성공적으로 사용해 본 사용자(프로젝트 소유자와 프로젝트 사용자 모두)로부터 명시적인 긍정적 보고를 수집하는 것이 이상적입니다.

초록

사용자가 예상한 패키지 대신 악성 패키지가 설치되는 의존성 혼동 공격은 점점 더 흔해지는 공급망 위협입니다. 최근 PyTorch 사건을 비롯하여 Python 의존성을 대상으로 하는 이러한 공격의 대부분은 여러 패키지 저장소를 사용할 때 발생합니다. 즉, 한 저장소(예: 사용자 지정 색인)에서 제공될 것으로 예상한 의존성이 다른 저장소(예: PyPI)에서 설치됩니다.

이 문제를 해결하는 데 도움을 주기 위해, 이 PEP는 Simple Repository API를 확장하여 저장소 운영자가 자신의 저장소에서 발견된 프로젝트가 다른 저장소의 프로젝트를 “추적한다”고 표시할 수 있도록 하고, 프로젝트가 여러 저장소에 걸쳐 네임스페이스를 확장할 수 있도록 제안합니다.

이러한 기능을 사용하면 설치 도구가 특정 저장소 조합에서 제공되는 프로젝트가 예상된 것이므로 허용해야 하는 경우와 그렇지 않아 사용자를 보호하기 위해 오류와 함께 설치를 중단해야 하는 경우를 판단할 수 있습니다.

동기

“의존성 혼동” 공격이라고 하는 오래된 공격 유형이 있으며, 이는 대략 개별 사용자가 패키지 A를 받으리라 예상했지만 대신 B를 받는 상황으로 요약됩니다. Python에서는 여러 저장소(기본 PyPI를 포함할 수도 있음)의 구성 때문에 이러한 일이 거의 항상 발생합니다. 사용자는 패키지 A가 저장소 X에서 제공되기를 예상했지만, 누군가가 동일한 이름으로 패키지 B를 저장소 Y에 게시할 수 있기 때문입니다.

의존성 혼동 공격은 오래전부터 가능했지만, 최근에는 이러한 공격이 성공적으로 실행된 사례의 공개 예시로 인해 널리 보도되었습니다.

구체적인 예로, 최근 PyTorch 프로젝트에 torchtriton이라는 내부 패키지가 있었는데, 이는 원래 https://download.pytorch.org/에 위치한 해당 프로젝트의 저장소에서만 설치되도록 의도되었습니다. 그러나 이 저장소는 PyPI와 함께 사용되도록 설계되었고 torchtriton이라는 이름이 PyPI에서 선점되지 않았기 때문에 공격자가 해당 이름을 사용하여 악성 버전을 게시할 수 있었습니다.

현재 이러한 공격을 완화하는 방법은 여러 가지가 있지만, 모두 기본적으로 보호받는 대신 최종 사용자가 직접 자신을 보호하기 위해 추가 조치를 취해야 합니다. 이는 대다수 사용자가 이러한 유형의 공격을 결국 인지하더라도 여전히 취약한 상태로 남을 가능성이 높다는 의미입니다.

궁극적으로 이러한 공격의 근본 원인은 모든 Python 패키지 이름이 비롯되는 전역적으로 고유한 네임스페이스가 없다는 사실입니다. 대신 각 저장소는 서로 구별되는 자체 네임스페이스를 가지며, 설치 도구는 설치할 spam과 같은 “추상” 이름을 받으면 이를 pypi.org:spam 또는 example.com:spam 과 같은 “구체적인” 이름으로 암묵적으로 변환해야 합니다. 현재 Python 설치 도구의 표준 동작은 이러한 여러 네임스페이스를 모든 네임스페이스의 파일을 포함하는 하나의 네임스페이스로 암묵적으로 평탄화하는 것입니다.

네임스페이스를 병합하는 것이 예상된 동작이라는 이러한 가정 때문에, 서로 다른 저장소에서 동일한 이름을 가진 패키지를 서로 다른 주체가 작성한 경우(torchtriton사례와 같이) 의존성 혼동 공격이 가능해집니다.

이 문제는 특히 “올바른” 답이 없다는 점에서 까다롭습니다. 두 저장소를 하나의 네임스페이스로 병합하려는 유효한 사용 사례 두 저장소를 서로 구별되는 네임스페이스로 취급하려는 유효한 사용 사례가 모두 존재합니다. 따라서 설치 도구에는 여러 저장소의 네임스페이스를 언제 병합해야 하고 언제 병합하지 않아야 하는지 판단할 수 있는 메커니즘이 필요하며, 무조건 항상 병합하거나 절대 병합하지 않는 규칙만으로는 충분하지 않습니다.

궁극적으로 어떤 저장소에서 무엇이 설치되는지에 대한 기대가 실제로 중요한 사람은 최종 사용자이므로, 이 기능을 최종 사용자에게 직접 맡길 수도 있습니다. 그러나 저장소가 안전한 경우를 표시할 수 있도록 저장소 명세를 확장하면, 프로젝트가 자연스럽게 여러 개의 서로 구별되는 네임스페이스에 걸쳐 있더라도 개별 프로젝트와 저장소가 “기본적으로 작동”하도록 할 수 있으며, 동시에 설치 도구가 기본적으로 안전할 수 있는 능력도 유지할 수 있습니다.

이 PEP 자체가 의존성 혼동 공격을 해결하는 것은 아니지만, 유효하고 안전한 사용 사례에 과도한 부수 피해를 일으키지 않으면서 설치 도구가 이러한 공격을 방지할 수 있을 만큼 충분한 정보를 제공합니다.

근거

이 PEP가 가능하게 하려는 저장소 간 이름 병합에는 두 가지 주요 사용 사례가 있습니다.

첫 번째 사용 사례는 한 저장소가 자체 이름을 정의하는 것이 아니라 다른 저장소에서 정의된 이름을 확장하는 경우입니다. 이는 프로젝트를 한 저장소에서 다른 저장소로 미러링하는 경우( Bandersnatch 참조) 또는 저장소가 특정 플랫폼을 위한 보충 아티팩트를 제공하는 경우( Piwheels 참조)에 흔히 발생합니다.

이 경우 확장되는 저장소나 프로젝트는 자신이 확장되고 있다는 사실이나 누가 확장하는지 알지 못할 수 있으므로, 이는 “확장하는” 저장소 자체에 없는 정보에 의존할 수 없습니다.

두 번째 사용 사례는 프로젝트가 하나의 “주” 저장소에 게시하려 하지만, 추가 플랫폼, GPU, CPU 등을 위한 바이너리를 제공하는 추가 저장소를 두려는 경우입니다. 현재 휠 태그로는 이러한 유형의 바이너리 호환성을 충분히 표현할 수 없으므로, 이를 활용하려는 프로젝트는 여러 저장소를 설정하고 사용자가 자신의 플랫폼, GPU, CPU 등에 맞는 올바른 바이너리를 얻도록 저장소를 수동으로 구성하게 해야 합니다.

이 사용 사례는 첫 번째 사용 사례와 유사하지만, 이를 독립적인 사용 사례로 구분하게 만드는 중요한 차이는 누가 정보를 제공하며 그 신뢰 수준이 어떠한가입니다.

사용자가 특정 저장소를 구성하거나 기본값에 의존할 때에는 어떤 저장소를 의미하는지 모호하지 않습니다. 저장소는 URL로 식별되며, 도메인 시스템을 통해 URL은 전역적으로 고유한 식별자입니다. 이러한 비모호성으로 인해 설치 프로그램은 저장소 운영자를 신뢰할 수 있다고 가정하고, 검증할 필요 없이 운영자가 제공하는 메타데이터를 신뢰할 수 있습니다.

반대로 설치 프로그램이 여러 저장소에서 이름을 발견하면 어느 저장소를 신뢰해야 하는지가 모호합니다. 이러한 모호성 때문에 설치 프로그램은 어느 저장소의 프로젝트 소유자도 신뢰할 수 있다고 가정할 수 없으며, 실제로 동일한 프로젝트인지와 둘 중 하나가 의존성 혼동 공격이 아닌지를 검증해야 합니다.

설치 프로그램이 여러 저장소 간의 메타데이터를 검증할 방법이 없다면, 프로젝트는 이 사용 사례를 안전하게 지원하기 위해 저장소 운영자가 되어야 합니다. 이는 특별히 잘못된 선택은 아니지만, 저장소가 프로젝트 소유자가 이러한 관계를 안전하게 표현할 방법을 제공하지 않으면 프로젝트 소유자가 대신 저장소 운영자의 메타데이터를 사용하도록 유도될 위험이 있으며, 이는 원래의 보안 취약성을 다시 초래합니다.

사양

이 사양은 단순 저장소 API 버전 1.2의 변경 사항을 정의하며, 새로운 메타데이터 항목 두 가지인 저장소 “Tracks”와 “Alternate Locations”를 추가합니다.

저장소 “Tracks” 메타데이터

한 저장소가 다른 저장소에 호스팅된 프로젝트를 “확장”하도록 의도된 프로젝트를 호스팅할 수 있게 하기 위해, 이 PEP는 확장하는 저장소가 확장 대상 프로젝트와 저장소의 URL을 추가하여 특정 프로젝트가 다른 저장소의 프로젝트를 “추적”한다고 선언할 수 있도록 합니다.

이는 JSON에서는 키 meta.tracks로 노출되고, HTML에서는 프로젝트별 URL($root/$project/)의 pypi:tracks라는 이름의 meta 요소로 노출됩니다.

이 메타데이터를 사용할 때 몇 가지 핵심 속성을 반드시 보존해야 합니다:

  • 이는 해당 저장소를 사용하는 개별 게시자가 아니라 저장소 운영자 자체의 통제하에 MUST 있어야 합니다.
    • “저장소 운영자”에는 특정 저장소의 전체 네임스페이스를 관리하는 사람도 포함될 수 있으며, 이는 한 주체가 소프트웨어를 운영하지만 다른 주체가 해당 저장소의 전체 네임스페이스를 소유하거나 관리하는 호스팅 저장소 서비스와 같은 상황에서 해당할 수 있습니다.
  • 모든 URL은 확장하는 저장소의 프로젝트와 동일한 “프로젝트”를 나타내야 MUST 합니다.
    • 이는 해당 URL이 동일한 파일을 제공해야 한다는 뜻은 아닙니다. 서로 다른 플랫폼에서 빌드된 바이너리, 로컬 패치가 적용된 사본 등을 포함하는 것은 유효합니다. 정확히 무엇이 “동일한” 프로젝트를 구성하는지는 궁극적으로 사용자가 저장소와 그 운영자에게 기대하는 바에 달려 있으므로, 이는 의도적으로 모호하게 두었습니다.
  • 해당 네임스페이스를 “소유하는” 저장소를 가리켜야 MUST 하며, 그 네임스페이스를 추적하는 다른 저장소를 가리켜서는 안 됩니다.
  • 정규화 후 정확히 동일한 이름의 프로젝트를 가리켜야 MUST 합니다.
  • 확장된 저장소의 기본 URL이 아니라 해당 프로젝트의 실제 URL을 가리켜야 MUST 합니다.

저장소의 모든 이름이 동일한 저장소를 추적해야 하거나, 모든 이름이 아예 저장소를 추적해야 하는 것은 아닙니다. 일부 이름은 저장소를 추적하고 일부 이름은 추적하지 않는 혼합 사용 저장소도 명시적으로 허용됩니다.

JSON

{
  "meta": {
    "api-version": "1.2",
    "tracks": ["https://pypi.org/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"]
  },
  "name": "holygrail",
  "files": [
    {
      "filename": "holygrail-1.0.tar.gz",
      "url": "https://example.com/files/holygrail-1.0.tar.gz",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "yanked": "Had a vulnerability"
    },
    {
      "filename": "holygrail-1.0-py3-none-any.whl",
      "url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "dist-info-metadata": true
    }
  ]
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta name="pypi:repository-version" content="1.2">
    <meta name="pypi:tracks" content="https://pypi.org/simple/holygrail/">
    <meta name="pypi:tracks" content="https://test.pypi.org/simple/holygrail/">
  </head>
  <body>
    <a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
    <a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
  </body>
</html>

“Alternate Locations” 메타데이터

이 PEP는 프로젝트가 여러 저장소에 걸쳐 네임스페이스를 확장할 수 있도록, 프로젝트 소유자가 해당 프로젝트의 “대체 위치” 목록을 선언할 수 있게 합니다. 이는 JSON에서는 alternate-locations 키로, HTML에서는 pypi-alternate-locations라는 이름의 메타 요소로 노출되며, 이 요소는 여러 번 사용할 수 있습니다.

이 메타데이터를 사용할 때 준수해야 하는 몇 가지 주요 속성이 반드시 있습니다:

  • 이 메타데이터를 신뢰하려면 해당 프로젝트가 발견되는 모든 위치에서 대체 위치가 무엇인지에 대해 합의가 반드시 이루어져야 합니다.
  • 대체 위치를 사용할 때 클라이언트는 응답을 가져온 URL이 목록에 포함되어 있다고 암묵적으로 반드시 가정해야 합니다. 즉, https://pypi.org/simple/foo/에서 가져온 응답에 alternate-locations 메타데이터가 ["https://example.com/simple/foo/"]라는 값을 가진 경우, 해당 값이 ["https://example.com/simple/foo/", "https://pypi.org/simple/foo/"]인 것처럼 반드시 처리해야 합니다.
  • 배열 내 요소의 순서는 특별한 의미를 갖지 않습니다.

설치 프로그램은 대체 위치 메타데이터를 사용하는 프로젝트를 발견하면, 명명된 모든 저장소가 여러 저장소에 걸쳐 동일한 네임스페이스를 확장한다고 고려해야 합니다.

Note

이 대체 위치 메타데이터는 아티팩트 수준 메타데이터가 아니라 프로젝트 수준 메타데이터입니다. 따라서 핵심 메타데이터 사양의 일부로 포함되지 않고, 각 저장소가 지원하기로 선택한 경우 이를 위한 구성 옵션을 제공해야 하는 항목입니다.

JSON

{
  "meta": {
    "api-version": "1.2"
  },
  "name": "holygrail",
  "alternate-locations": ["https://pypi.org/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"],
  "files": [
    {
      "filename": "holygrail-1.0.tar.gz",
      "url": "https://example.com/files/holygrail-1.0.tar.gz",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "yanked": "Had a vulnerability"
    },
    {
      "filename": "holygrail-1.0-py3-none-any.whl",
      "url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "dist-info-metadata": true
    }
  ]
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta name="pypi:repository-version" content="1.2">
    <meta name="pypi:alternate-locations" content="https://pypi.org/simple/holygrail/">
    <meta name="pypi:alternate-locations" content="https://test.pypi.org/simple/holygrail/">
  </head>
  <body>
    <a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
    <a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
  </body>
</html>

권장 사항

이 절은 비규범적이며, 이 PEP가 기본적으로 사용자를 보호하면서 기존 워크플로의 중단을 최소화하는 최상의 절충안을 제공한다고 판단하는 방식으로 설치 프로그램이 이 메타데이터를 해석하도록 권장 사항을 제공합니다. 이러한 권장 사항은 구속력이 없으며, 설치 프로그램은 이를 무시하거나 각자의 특정 상황에 적합하다고 판단되는 경우 선택적으로 적용할 수 있습니다.

파일 검색 알고리즘

Note

이 알고리즘은 현재 pip가 파일을 검색하는 방식을 기반으로 작성되었으며, 다른 설치 프로그램은 자체 검색 절차에 따라 이를 조정할 수 있습니다.

현재 “표준” 파일 검색 알고리즘은 대략 다음과 같습니다:

  1. 구성된 모든 저장소에 걸쳐 모든 파일의 목록을 생성합니다.
  2. 잠금 파일 또는 요구 사항 파일의 알려진 해시와 일치하지 않는 파일을 모두 필터링하여 제외합니다.
  3. 현재 플랫폼, Python 버전 등에 맞지 않는 파일을 모두 필터링하여 제외합니다.
  4. 해당 파일 목록을 리졸버에 전달하면, 리졸버는 파일이 어느 저장소에서 왔는지와 관계없이 해당 파일 중 “최적”의 일치를 찾으려고 시도합니다.

설치 프로그램이 새로운 메타데이터를 고려하도록 파일 검색 알고리즘을 변경하고, 대신 다음과 같이 수행하는 것이 권장됩니다:

  1. 구성된 모든 저장소에 걸쳐 모든 파일의 목록을 생성합니다.
  2. 잠금 파일 또는 요구 사항 파일의 알려진 해시와 일치하지 않는 파일을 모두 필터링하여 제외합니다.
  3. 최종 사용자가 설치 프로그램에 특정 저장소에서 프로젝트를 가져오도록 명시적으로 지시한 경우, 다른 모든 저장소를 필터링하여 제외하고 5번으로 건너뜁니다.
  4. 발견된 파일이 여러 저장소에 걸쳐 있는지 확인하십시오. 그렇다면 “Tracks” 또는 “Alternate Locations” 메타데이터 중 하나가 파일이 발견된 모든 저장소를 함께 안전하게 병합하도록 허용하는지 판단하십시오. 해당 메타데이터가 이를 허용하지 않는 경우 오류를 생성하고, 그렇지 않으면 계속 진행합니다.
    • 참고: 이는 원격 저장소에만 적용됩니다. 로컬 파일 시스템에 존재하는 저장소는 모든 원격 저장소와 병합되는 것이 항상 암묵적으로 허용되어야 합니다.
  5. 현재 플랫폼, Python 버전 등에 맞지 않는 파일을 모두 필터링하여 제외합니다.
  6. 해당 파일 목록을 리졸버에 전달하면, 리졸버는 파일이 어느 저장소에서 왔는지와 관계없이 해당 파일 중 “최적”의 일치를 찾으려고 시도합니다.

이는 다소 미묘하지만, 이 권장 사항의 핵심 사항은 다음과 같습니다:

  • 특정 “유효한” 아티팩트의 해시를 포함하는 잠금 파일 또는 요구 사항 파일을 사용하는 사용자는 이러한 해시 자체로 보호된다고 간주합니다. 나머지 권고 사항은 해시 생성 중에 적용되기 때문입니다. 따라서 알 수 없는 해시는 미리 필터링하여 제외합니다.
  • 사용자가 특정 저장소 집합에서 프로젝트를 가져오기를 원한다고 설치 도구에 명시적으로 알린 경우에는 이를 의심할 이유가 없으므로, 해당 사용자가 네임스페이스를 병합해도 안전하도록 확인했다고 간주합니다.
  • 해당 프로젝트가 단일 저장소에서만 제공되는 경우에는 의존성 혼동의 가능성이 없으므로, 허용하는 것 외에는 아무 작업도 할 이유가 없습니다.
  • 플랫폼, Python 버전 등을 기준으로 필터링하기 전에 이 PEP의 메타데이터를 확인합니다. 특정 플랫폼, Python 버전 등에서만 나타나는 오류를 원하지 않기 때문입니다.
  • 네임스페이스를 병합해도 안전하다는 것을 알려 주는 내용이 없다면, 암묵적으로 안전하다고 간주하지 않고 대신 오류를 생성합니다.
  • 그 외의 경우에는 네임스페이스를 병합하고 계속 진행합니다.

이 알고리즘은 설치 도구가 서로 다른 두 네임스페이스를 하나로 평탄화할 수 있다고 절대 가정하지 않도록 보장합니다. 이는 사실상 모든 종류의 의존성 혼동 공격 가능성을 제거하면서도, 서로 다른 네임스페이스가 실제로 안전하게 병합할 수 있는 하나의 논리적 네임스페이스인 경우를 사람들이 명시적으로 선언할 수 있도록 스택 전반에서 안전한 방식으로 권한을 제공합니다.

위 알고리즘은 대부분 개념적 모델입니다. 실제로는 개인 정보 보호를 강화하고 속도를 높이기 위해 알고리즘이 약간 달라질 수 있으며, 특정 설치 도구에 더 적합하도록 조정될 수도 있습니다.

최종 사용자를 위한 명시적 구성

이 PEP는 설치 도구가 최종 사용자에게 특정 패키지를 정확히 어떤 저장소에서 설치할지 구성하도록 허용하는 특정 메커니즘을 규정하거나 권장하지 않습니다. 그러나 설치 도구가 최종 사용자에게 해당 구성을 제공할 수 있는 어떤 메커니즘은 제공할 것을 권장합니다. 그러한 메커니즘이 없으면 사용자는 torchtriton과 같은 경우에 외부에서 네임스페이스 충돌을 해결하지 않는 한 완전히 작동하지 않는 DoS 상황에 처할 수 있기 때문입니다(한 저장소에서 해당 이름을 삭제하도록 요청하거나, 병합을 처리하는 개인 저장소를 세우는 등의 방법이 있습니다).

또한 이 구성을 사용하면 기본 동작이 안전해질 때까지 이어질 가능성이 높은 긴 전환 기간 동안 최종 사용자가 선제적으로 자신을 보호할 수 있습니다.

이를 전달하는 방법

Note

이 예제는 pip에 특화되어 있으며 pip가 이 PEP를 구현하기 위해 선택할 구체적인 방법을 전제로 합니다. 이 예제는 이러한 변경 사항을 전달할 수 있는 방법의 예로 포함된 것이며, pip 또는 다른 설치 도구가 이를 구현하는 방식을 제한하려는 의도는 없습니다. 궁극적으로 이것이 실제 전달의 기반이 될 수 있으며, 그렇다면 정확성과 명확성을 위해 편집해야 합니다.

이 섹션은 블로그 게시물, 이메일 또는 Discourse 게시물에 사용할 수 있는 이러한 변경 사항을 전달하는 하나의 완전한 “게시물”인 것처럼 읽어야 합니다.

오랫동안 존재해 온 “의존성 혼동” 공격이라는 공격 유형이 있으며, 이는 대략 개인이 패키지 A를 얻을 것으로 예상했지만 대신 B를 얻는 상황으로 요약됩니다. Python에서는 최종 사용자가 여러 저장소를 구성했기 때문에 이러한 일이 거의 항상 발생합니다. 최종 사용자는 패키지 A가 저장소 X에서 제공될 것으로 예상하지만, 누군가 저장소 Y에 패키지 A와 같은 이름의 패키지 B를 게시할 수 있습니다.

현재 이러한 공격을 완화하는 방법은 여러 가지가 있지만, 본질적으로 안전한 것이 아니라 최종 사용자가 스스로를 보호하기 위해 명시적으로 추가 조치를 취해야 한다는 공통점이 있습니다.

pip 사용자를 보호하고 이러한 유형의 공격으로부터 지키기 위해, pip가 설치할 패키지를 검색하는 방식을 변경할 예정입니다.

무엇이 변경됩니까?

pip가 동일한 프로젝트를 여러 원격 저장소에서 사용할 수 있음을 발견하면, 기본적으로 어느 저장소가 설치에 적합한지 추측하는 대신 오류를 생성하고 진행을 거부합니다.

여러 저장소에 기본적으로 게시하는 프로젝트에는 해당 저장소들이 함께 사용될 때 pip가 오류를 발생시키지 않도록 저장소를 안전하게 “연결”할 수 있는 기능이 제공됩니다.

pip의 최종 사용자에게 특정 프로젝트에 유효한 저장소를 하나 이상 명시적으로 정의할 수 있는 기능이 제공됩니다. 그러면 pip는 해당 프로젝트에 대해 해당 저장소만 고려하므로 오류가 전혀 생성되지 않습니다.

자세한 내용은 TBD를 참조하십시오.

누가 영향을 받습니까?

여러 원격 저장소(예: 로컬 파일 시스템에 존재하지 않는 저장소)에서 설치하는 사용자는 다음과 같은 경우 pip가 성공적으로 설치하는 대신 오류를 발생시킬 수 있으므로 영향을 받을 수 있습니다.

  • 동일한 “이름”이 여러 원격 저장소에서 제공되는 프로젝트를 설치합니다.
  • 여러 원격 저장소에서 사용할 수 있는 프로젝트 이름이 해당 저장소들을 서로 연결하기 위해 정의된 메커니즘 중 하나를 사용하지 않았습니다.
  • pip를 호출하는 사용자가 특정 프로젝트에 유효한 저장소를 명시적으로 제어하기 위해 정의된 메커니즘을 사용하지 않았습니다.

여러 원격 저장소를 사용하지 않는 사용자는 전혀 영향을 받지 않으며, 여기에는 단일 원격 저장소와 로컬 파일 시스템 “wheel house”만 사용하는 사용자도 포함됩니다.

무엇을 해야 합니까?

pip 사용자라면?

단일 원격 저장소만 사용하는 경우에는 아무것도 하지 않아도 됩니다.

여러 원격 저장소를 사용하는 경우, pip 호출에 --use-feature=TBD를 추가하여 새로운 동작을 선택하고, 종속 항목 중 여러 원격 저장소에서 제공되는 항목이 있는지 확인할 수 있습니다. 그런 항목이 있다면, 그렇게 된 이유와 자신에게 가장 적절한 해결 단계를 파악하기 위해 해당 항목을 감사해야 합니다.

이 동작이 기본값이 되면, pip 호출에 --use-deprecated=TBD를 추가하여 일시적으로 이 동작을 선택 해제할 수 있습니다.

공개 저장소에서 호스팅되지 않는 프로젝트를 사용하지만 공개 저장소를 대체 저장소로 계속 사용하는 경우, 저장소 파일로 pip를 구성하여 해당 종속 항목이 어디에서 제공되어야 하는지 명시하는 것을 고려하십시오. 그러면 공개 저장소에 해당 이름이 등록되어 pip에서 오류가 발생하는 일을 방지할 수 있습니다.

프로젝트 소유자라면?

프로젝트를 단일 저장소에만 게시한다면 아무것도 하지 않아도 됩니다.

동시에 함께 사용하도록 의도된 여러 저장소에 프로젝트를 게시한다면, 최종 사용자의 문제를 방지하기 위해 모든 저장소가 대체 저장소 메타데이터를 제공하도록 구성하십시오.

프로젝트를 단일 저장소에 게시하지만 다른 저장소와 함께 사용되는 경우가 많다면, 제3자가 사용자의 pip install호출이 실패하기 시작하도록 만들 수 없게 해당 저장소에 이름을 선제적으로 등록하는 것을 고려하십시오. 프로젝트 이름이 너무 일반적이거나 저장소에 방어적 이름 선점을 방지하는 정책이 있다면 이 방법을 이용할 수 없을 수도 있습니다.

저장소 운영자라면?

최종 사용자가 저장소를 어떤 방식으로 사용하기를 의도하는지, 그리고 최종 사용자가 저장소를 어떻게 사용하기를 원하는지 결정해야 합니다.

비공개 프로젝트를 호스팅하는 비공개 저장소의 경우, 사용자가 의존하는 공개 프로젝트를 자체 저장소로 미러링하는 것이 권장됩니다. 이때 공개 프로젝트가 비공개 프로젝트와 병합되지 않도록 주의하고, 사용자에게 --index-url옵션을 사용하여 자신의 저장소만 사용하도록 안내하십시오.

공개 프로젝트를 호스팅하는 공개 저장소의 경우, 대체 저장소 메커니즘을 구현하고 해당 프로젝트를 둘 이상의 저장소에서 제공하는 경우 프로젝트 소유자가 프로젝트를 이용할 수 있는 저장소 목록을 구성할 수 있도록 하십시오.

다른 저장소를 “추적”하지만 특정 플랫폼용으로 빌드된 휠과 같은 추가 아티팩트를 제공하는 공개 저장소의 경우, 해당 저장소에 “tracks” 메타데이터를 구현해야 합니다. 그러나 이 정보는 귀하의 저장소에 프로젝트를 게시하는 최종 사용자가 설정할 수 없어야 합니다. 자세한 내용은 TBD를 참조하십시오.

거부된 아이디어

참고: 이러한 아이디어 중 일부는 pip에 다소 특화되어 있지만, pip에서 작동하지 않는 해결책은 그다지 유용한 해결책이 아닙니다.

파일 목록이 동일할 때 미러를 암묵적으로 허용하기

모든 저장소가 정확히 동일한 파일 목록을 반환한다면, 해당 저장소를 동일한 네임스페이스로 간주하여 암묵적으로 병합해도 안전합니다. 이렇게 하면 사용자나 저장소 운영자가 별도로 작업하지 않아도 미러를 자동으로 허용할 수 있습니다.

안타깝게도 이 방식에는 바람직하지 않게 만드는 두 가지 문제가 있습니다.

  • 이 방식은 서로 정확히 복사된 미러의 경우만 해결하며, 더 일반적인 해결책이 되는 다른 저장소를 “추적”하는 저장소에는 적용되지 않습니다.
  • 정확히 동일한 미러의 경우에도 서로를 미러링하는 여러 저장소는 분산 시스템이므로 서로 항상 완전히 일관되지는 않으며, 사실상 최종 일관성 시스템이 됩니다. 따라서 이 암묵적 휴리스틱에 의존하는 저장소는 원본 저장소와 미러 저장소 간의 차이로 인해 간헐적으로 실패하게 됩니다.

저장소의 순서를 지정하는 메커니즘 제공하기

저장소에 순서를 부여하고, 프로젝트에 대한 파일을 제공하는 첫 번째 저장소를 찾으면 검색 알고리즘을 중단하는 메커니즘을 제공하는 것도 실행 가능한 해결책이며, 순서가 올바르게 지정되면 안전합니다.

그러나 이 방식은 여러 가지 이유로 거부되었습니다.

  • 사용자가 지정하는 저장소의 순서가 의미가 없으며 사실상 순서가 정의되어 있지 않다는 점을 15년 넘게 교육해 왔습니다. 이제 와서 순서가 중요하다고 말하기 시작하는 것은 번복하기 어려울 것입니다.
  • 사용자는 단일 위치 내에서 저장소를 지정하는 순서를 쉽게 재배열할 수 있지만, 여러 위치(env var, conf file, requirements file, cli arguments)에서 저장소를 불러올 때의 순서는 pip에 하드 코딩되어 있습니다. 이는 결정적이고 문서화된 순서이겠지만, 사용자가 저장소를 정의하기를 원하는 순서라고 가정할 이유는 없으며, 암시적 순서가 올바른 순서가 되도록 pip를 구성하는 방식을 억지로 변경해야 합니다.
  • 위 문제는 저장소가 정의된 순서를 암묵적으로 사용하는 대신 순서를 명시적으로 선언하는 방법을 제공하여 완화할 수 있습니다. 그러나 그렇게 하면 사용자가 명시적으로 구성하지 않는 한 보호 기능이 제공되지 않는다는 의미가 됩니다.
  • 순서에 의존하면 프로젝트별로 결정할 방법 없이 한 저장소가 다른 저장소보다 항상우선된다고 가정하게 됩니다.
  • 순서에 의존하는 것은 미묘한 문제입니다. 저장소의 순서를 살펴보더라도 어떤 이름이 어떤 저장소에서 제공될지 미리 알거나 보장할 방법이 없습니다. 그 순간에는 어떤 저장소가 어떤 이름을 제공하는지만 알 수 있습니다.
  • 순서에 의존하는 것은 취약합니다. 서로 무관한 두 저장소에서 무작위로 이름 충돌이 발생하지 않을 것이라고 가정할 이유는 없습니다. 낮은 우선순위의 저장소에서 라이브러리를 사용하고 있는데, 더 높은 우선순위의 저장소가 우연히 충돌하는 이름을 사용하기 시작하면 어떻게 되겠습니까?
  • 순서가 잘못된 결과를 초래하는 경우에도 사용자에게 어떠한 피드백도 제공하지 않은 채 조용히 그렇게 처리됩니다. 이는 의도된 동작입니다. 실제로 무엇이 잘못된 결과이고 무엇이 올바른 결과인지 알지 못하며, 순서가 올바른 결과를 제공하기를 기대할 뿐이기 때문입니다. 올바른 결과가 나오면 아무것도 손상시키지 않고 사용자를 보호할 수 있습니다. 그러나 잘못된 결과가 나오면 pip에서 발생한 매우 혼란스러운 동작만 사용자에게 남으며, pip는 단지 잘못된 대상을 조용히 설치합니다.

이 아이디어에는 실질적인 문제가 PyPI의 개방형 이름 등록 특성에서 비롯된다고 보는 변형이 있습니다. 따라서 “기본” 저장소를 제외한 모든 저장소를 동일한 우선순위로 취급하고, 기본 저장소의 우선순위를 더 낮게 취급하면 문제를 해결할 수 있다는 것입니다.

이 방식이 실제로 문제를 개선한다는 점에서는 맞지만, 일반적인 순서 지정 아이디어와 동일한 문제를 많이 안고 있습니다(모두 그런 것은 아닙니다).

또한 PyPI 또는 “기본”으로 구성된 다른 저장소가 이름을 개방적으로 등록할 수 있는 유일한 저장소라고 가정합니다. 그러나 사용자가 PyPI와 함께 사용할 것으로 예상되는 Piwheels 같은 프로젝트도 존재하며, PyPI에 등록된 모든 이름을 추적하므로 사실상 이름을 개방적으로 등록할 수 있습니다.

저장소 프록시에 의존하기

한 가지 가능한 해결책은 이 문제를 설치 프로그램이 해결하도록 하는 대신, 여러 저장소를 지능적으로 안전하게 병합할 수 있는 저장소 프록시에 의존하는 것입니다. 저장소 프록시는 문제 영역에 특화된 구성과 기능을 제공할 수 있으므로, 복잡한 요구 사항이 있는 사용자에게 더 나은 경험을 제공할 수 있습니다.

그러나 다음과 같은 이유로 이 방안은 거부되었습니다.

  • 사용자가 이를 사용하도록 선택해야 합니다. 또는 여러 저장소가 필요한 사용자가 저장소 프록시를 사용하도록 강제하기 위해 설치 프로그램에서 둘 이상의 저장소를 사용할 수 있는 기능을 제거해야 합니다.
    • 둘 이상의 저장소를 구성할 수 있는 기능을 제거하는 방안은 최종 사용자에게 너무 큰 영향을 주므로 거부되었습니다.
  • 사용자는 서로 다른 컨텍스트에서 여러 저장소를 병합한 서로 다른 결과를 필요로 할 수 있으며, 서로 다르고 상호 배타적인 저장소를 병합해야 할 수도 있습니다. 이는 각 고유한 옵션 집합에 대해 실제로 여러 저장소 프록시를 설정해야 한다는 의미입니다.
  • 사용자가 인프라를 유지 관리해야 하거나, 설치 프로그램에 각 호출마다 저장소를 자동으로 시작하는 기능을 추가해야 합니다.
  • 이 방식은 이러한 문제에 대한 해결책이 필요하다는 요구 사항을 실제로 변경하지 않습니다. 구현 책임을 설치 프로그램에서 일부 저장소 프록시로 옮길 뿐이며, 어느 경우든 서로 다른 네임스페이스를 병합하는 방법을 결정하는 무언가가 여전히 필요합니다.
  • 궁극적으로 대부분의 사용자는 여러 저장소와 안전하게 상호 작용하기 위해 저장소 프록시를 별도로 구축해야 하는 것을 원하지 않습니다.

해시 검사에만 의존하기

해시 검사를 사용하는 또 다른 가능한 해결책이 있습니다. 해시 검사가 활성화되어 있으면 사용자는 예상하지 못한 아티팩트를 가져올 수 없으므로 네임스페이스가 잘못 병합되는지 여부는 중요하지 않습니다.

이는 분명 해결책이지만, 실행 불가능하게 만드는 문제도 안고 있습니다.

  • 사용자가 이를 사용하도록 선택해야 하므로 기본적으로 사용자는 여전히 보호되지 않습니다.
  • 사용자가 자신의 해시를 관리하기 위해 많은 수작업을 해야 하며, 대부분의 사용자는 그렇게 하려고 하지 않을 가능성이 높습니다.
  • 사용자가 의존성의 출처로 requirements.txt 파일을 사용하지 않는 경우 보호 기능을 적용하기가 어렵고 장황합니다(이는 빌드 시간 의존성과 명령줄에서 제공되는 의존성에 영향을 줍니다).
  • 이 방법은 문제를 어느 정도만 해결할 뿐이며, 어떤 의미에서는 설치 프로그램이 사용할 해시를 생성하는 시스템으로 문제의 책임을 단순히 옮깁니다. 그 시스템이 사람이 수동으로 해시를 검증하는 시스템이 아니라면, 실제로 그럴 가능성은 낮지만, 네임스페이스를 병합하는 방법에 대한 질문을 해시의 유지 관리를 구현하는 도구로 옮긴 것에 불과합니다.

모든 프로젝트가 “default” 저장소에 존재하도록 요구하십시오

또 다른 아이디어는 --extra-index-url의 범위를 좁혀 기본 저장소를 보완하는 저장소를 참조하는 용도로만 지원하는 것입니다. 이는 기본 저장소가 네임스페이스를 정의하고 각 추가 저장소는 추가 패키지로 이를 확장한다는 의미입니다.

이를 구현하려면 대략적으로 추가 저장소가 작동하려면 프로젝트가 기본 저장소에 MUST등록되어 있어야 한다고 요구해야 합니다.

그런 방식으로 범위를 성공적으로 좁히면 어느 정도 작동하지만, 궁극적으로 다음과 같은 이유로 거부되었습니다.

  • 사용자는 이렇게 축소된 범위를 이해하거나 받아들이기 어려울 가능성이 높으며, 따라서 이제 지원되지 않는 방식으로 계속 사용하려고 할 가능성이 높습니다.
    • 범위가 이제 좁아졌다는 사실 때문에 이 문제는 더욱 복잡해집니다. 제외된 작업 흐름을 사용하는 사용자에게는 저장소 프록시를 설정하는 것 외에 더 이상 대안이 없으며, 이를 위해서는 이전에는 필요하지 않았던 인프라와 노력이 필요합니다.
  • “extra” 저장소의 이름이 기본 저장소의 이름과 같다는 이유만으로 두 이름이 같은 프로젝트라고 가정합니다. 완전히 새로운 생태계에서 처음부터 시작한다면 이러한 가정을 처음부터 세우고 정착시킬 수도 있겠지만, 생태계가 그 변화에 적응하도록 만드는 일은 극도로 어려울 것입니다.
    • 이는 이 접근 방식의 근본적인 문제입니다. 의존성 혼동을 일으키는 근본 원인은 서로 분리된 네임스페이스를 하나의 평면 네임스페이스로 평탄화하고 있기 때문입니다. 이 접근 방식은 본질적으로 이를 괜찮다고 선언하고, 모든 사용자에게 이름을 등록하도록 요구하여 완화하려고 합니다.
  • 위의 가정 때문에 추가 저장소의 이름이 기본 저장소와 우연히 충돌하는 경우, 해당 사용자에게는 작동하는 것처럼 보이지만 실제로는 자신도 모르게 의존성 혼동 상태에 놓이게 됩니다.
    • 이를 가능하게 하는 이름의 소유자는 해당 사용자에게 자신이 맡고 있는 역할을 전혀 알지 못할 것이며, 프로젝트를 삭제하거나 다른 사람에게 넘길 수도 있습니다. 그 결과 악의적인 사용자가 무심코 이를 탈취하도록 허용할 가능성이 있습니다.
  • 사용자는 방어적인 이름 선점을 위해 기본 저장소에 자신의 이름을 등록하여 작동하는 상태로 되돌리려고 할 가능성이 높습니다. 이를 수행할 수 있는지는 기본 저장소의 구체적인 정책, 이미 누군가 해당 이름을 가지고 있는지, 이름이 지나치게 일반적인지 등에 따라 달라집니다. 최선의 경우에도 내부적인 이름 사용을 확보하는 것 외에는 아무 목적이 없는 불필요한 자리 표시자 프로젝트가 생길 뿐입니다.

전역적으로 고유한 이름으로 이동하십시오

이 문제가 존재하는 주된 이유는 전역적으로 고유한 이름이 없기 때문입니다. 여러 네임스페이스 아래에 존재하는 로컬하게 고유한 이름을 하나의 평면 네임스페이스로 병합하려고 하고 있습니다. 대신 전역적으로 고유한 이름을 만들 방법을 고안할 수 있다면 전체 문제를 피해 갈 수 있습니다.

이 아이디어는 다음과 같은 이유로 거부되었습니다.

  • 사람에게도 의미가 있으면서 전역적으로 고유하고 안전한 이름을 생성하는 일은 어떤 종류의 중앙 집중식 데이터베이스에 의존하지 않고는 거의 불가능한 작업입니다. 제가 아는 한 이를 성공적으로 수행한 유일한 시스템들은 결국 도메인 시스템에 의존하여 패키지를 도메인 등이 포함된 URL로 참조합니다.
  • 전역적으로 고유한 이름을 얻을 메커니즘을 고안하더라도, 모든 것을 무너뜨리고 처음부터 다시 시작하지 않는 한 수십 년 된 시스템에 이를 소급 적용할 능력은 사실상 없습니다. 아마도 우리가 할 수 있는 최선은 전역적으로 고유하지 않은 모든 이름을 암묵적으로 PyPI 도메인 이름의 이름이라고 선언하고, PyPI가 아닌 패키지를 가진 모든 사용자에게 패키지 이름을 바꾸도록 강제하는 것입니다.
  • 이는 현재 시스템의 핵심 가정과 근본적인 부분을 너무나 많이 뒤엎게 되므로, 무엇부터 목록으로 작성하기 시작해야 할지조차 알기 어렵습니다.

설치 프로그램이 명시적 구성을 제공하도록 권장하십시오

제기된 한 가지 아이디어는 본질적으로 명시적 구성만 구현하고 다른 어떤 것도 변경하지 않는 것입니다. 구체적인 매핑 정책 제안이 실제로 명시적 구성 옵션에 영감을 주었고, 다음과 비슷한 파일을 만들었습니다.

{
  "repositories": {
    "PyTorch": ["https://download.pytorch.org/whl/nightly"],
    "PyPI": ["https://pypi.org/simple"]
  },
  "mapping": [
    {
      "paths": ["torch*"],
      "repositories": ["PyTorch"],
      "terminating": true
    },
    {
      "paths": ["*"],
      "repositories": ["PyPI"]
    }
  ]
}

명시적 구성을 사용하라는 권고는 이를 구현하는 방법에 대한 결정을 각 설치 프로그램에 맡겨 사용자를 위해 가장 적합한 방식을 선택할 수 있도록 합니다.

궁극적으로 어떤 형태의 명시적 구성만 구현하는 방안은 그 본질상 옵트인 방식이므로 거부되었습니다. 따라서 기존 도구로 문제를 해결할 능력이 가장 부족한 일반 사용자를 보호하지 못합니다. 명시적 구성과 함께 추가적인 보호 기능을 제공하면 모든 사용자를 기본적으로 보호할 수 있습니다.

또한 명시적 구성에만 의존한다는 것은 PyPI, Piwheels, PyTorch 등의 미러와 같은 경우에도 모든 최종 사용자가 동일한 문제를 계속해서 반복적으로 해결해야 한다는 의미입니다. 각각의 경우에 보안을 확보하려면 사용자는 앉아서 결정을 내리거나(또는 어떤 예제를 무비판적으로 모방할지 찾아야) 합니다. 여기에 추가 기능을 더하면 가능한 곳에서 이러한 보호 기능을 중앙 집중화하면서도, 숙련된 최종 사용자에게 자신의 운명을 완전히 통제할 수 있는 능력을 계속 제공할 수 있습니다.

npm식 스코프

npm이 구현한 방식과 유사한 스코프가 궁극적으로 이 문제를 해결할 수 있다는 제안이 있었습니다. 궁극적으로 스코프는 이 문제에 관해 아무것도 바꾸지 않습니다. 제가 아는 한 npm의 스코프는 전역적으로 고유하지 않으며, 스코프가 없는 이름과 마찬가지로 특정 레지스트리에 연결됩니다. 그러나 스코프가 가능하게 하는 것은 관련 프로젝트를 그룹화하기 위한 명확한 메커니즘과 npm.org의 사용자 또는 조직이 스코프 전체를 소유할 수 있는 기능입니다. 이를 통해 네임스페이스의 작지만 완전한 일부가 자신에게 속한다고 확신할 수 있으므로 명시적 구성을 훨씬 쉽게 처리할 수 있으며, 전체 스코프를 특정 비공개 레지스트리에 할당하는 규칙을 쉽게 작성할 수 있습니다.

안타깝게도 이는 기본적으로 명시적 구성만 사용하는 아이디어를 더 쉽게 구현한 버전이 됩니다. 사람들이 자체 레지스트리를 사용하는 일이 특히 흔하지 않은 npm에서는 이 방식이 어느 정도 잘 작동하지만, Python에서는 바로 그렇게 하도록 권장합니다.

“명시적 구성” 정의 및 표준화

이 PEP는 설치 프로그램이 특정 프로젝트가 어느 저장소에서 왔는지 명시적으로 구성할 수 있는 메커니즘을 갖추도록 권장하지만, 그 메커니즘이 무엇인지는 정의하지 않습니다. 이는 각 설치 프로그램의 UX와 밀접하게 연관되어 있으며, 각 설치 프로그램이 특정 사용 사례에 적합하다고 판단하는 방식으로 해당 구성을 노출할 수 있도록 하려는 것이므로 의도적으로 정의하지 않습니다.

또한 해당 메커니즘을 정의하자는 아이디어가 나왔을 때, 다른 설치 프로그램 중 어느 것도 자신들을 위해 그 메커니즘을 정의하는 데 특별한 관심을 보이지 않았으며, 이를 자신들의 UX의 일부로 다루는 데 만족한다는 뜻을 내비쳤습니다.

마지막으로, 해당 메커니즘을 정의하기로 결정하더라도 이 PEP의 저장소 API 변경 사항에 포함시키기보다는 별도의 PEP로 다루어야 하며, 궁극적으로 이를 표준화하는 방향으로 나아가기로 결정한다면 향후 PEP가 될 수 있습니다.

감사의 말

이 PEP로 이어진 논의를 시작하게 해 주신 Trishank Kuppusamy에게 제안서를 통해 감사드립니다.

이 PEP의 아이디어에 관해 초기 피드백과 논의를 제공해 주신 Paul Moore, Pradyun Gedam, Steve Dower, Trishank Kuppusamy에게 감사드립니다.

문서 교정과 이 PEP의 구조 및 품질 개선에 도움을 주신 Jelle Zijlstra, C.A.M. Gerlach, Hugo van Kemenade, Stefano Rivera에게 감사드립니다.