이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › 보안 › 웹 공격과 방어
의존성과 공급망
내가 적지 않은 라이브러리는 어떻게 내 프로그램에 들어오고, 그 버전은 누가 정하나
빌드 파일에 라이브러리 한 줄을 적으면, 그 라이브러리가 필요로 하는 다른 라이브러리(전이 의존성)까지 함께 들어와 내 프로그램의 일부로 실행됩니다. 그래서 내가 고른 적 없는 라이브러리의 취약점도 내 문제가 됩니다. 버전을 범위로 적었는지 고정했는지에 따라 새로 올라온 버전이 자동으로 들어오기도 하고, 이름을 한 글자 잘못 적으면 전혀 다른 패키지가 들어오기도 합니다. 라이브러리가 만들어져 저장소를 거쳐 내 빌드에 들어오기까지의 길을 공급망이라고 하고, 그 길 어딘가에 악성 코드를 끼워 넣는 것이 공급망 공격입니다. 여기 나오는 라이브러리 이름과 취약점은 모두 설명을 위해 꾸민 것입니다.
일부만 실행으로 확인 · 꾸민 라이브러리로 만든 개념 모형. 어떤 버전이 들어오는지는 열다섯 가지 조합을 Gradle 8.14.3으로 실제 해석해 대조했고, 취약·악성·가짜라는 판정은 꾸민 줄거리
이 실습에 나오는 개념
- 전이 의존성
- 내가 빌드 파일에 직접 적은 라이브러리(직접 의존성)가 다시 필요로 해서 함께 들어오는 라이브러리다. 빌드 도구는 라이브러리마다 딸린 설명 파일을 읽어 필요한 것을 차례로 따라가며 가져오므로, 한 줄을 적어도 실제로 들어오는 라이브러리는 훨씬 많다. 전이 의존성도 내 프로그램 안에서 직접 의존성과 똑같이 실행되므로 그 취약점은 내 프로그램의 취약점이 된다. 빌드 파일에는 이름이 보이지 않기 때문에, 의존성 나무를 출력하는 명령으로 무엇이 어떤 경로로 들어왔는지 확인해야 한다.
- 버전 범위와 잠금 파일
- 의존성의 버전을 하나로 고정하지 않고 "2.x 가운데 가장 높은 버전"처럼 범위로 적으면, 빌드할 때마다 그 범위에서 가장 새로운 버전이 선택된다. 고친 버전을 자동으로 받는 장점이 있지만, 같은 코드를 빌드해도 날에 따라 다른 코드가 들어오고 문제가 있는 새 버전도 자동으로 들어온다. 잠금 파일은 한 번 정해진 버전을 전이 의존성까지 모두 적어 두고 다음 빌드부터 그 버전만 받게 하는 파일이다. 잠금 파일을 코드와 함께 관리하면 누가 언제 빌드해도 같은 결과가 나오고, 새 버전은 잠금 파일을 갱신할 때만 들어온다.
- 공급망 공격과 타이포스쿼팅
- 공급망 공격은 프로그램을 직접 공격하지 않고 그 프로그램이 가져다 쓰는 라이브러리나 빌드 과정에 나쁜 코드를 끼워 넣는 공격이다. 라이브러리의 배포 계정을 빼앗아 악성 코드가 든 새 버전을 올리는 것이 한 가지 방법이고, 타이포스쿼팅은 널리 쓰이는 이름과 한두 글자만 다른 이름으로 가짜 패키지를 올려 두고 오타를 기다리는 방법이다. 들어온 코드는 내 프로그램과 같은 권한으로 실행되므로 피해가 크다. 버전을 고정하고, 의존성의 이름과 출처를 확인하고, 알려진 취약점을 검사하는 도구를 빌드에 넣어 대비한다.
- CI/CD
- CI(지속적 통합)는 코드 변경을 자주 합치고, 합칠 때마다 자동으로 빌드와 테스트를 실행해 문제를 일찍 발견하는 것이다. CD(지속적 전달·배포)는 검증된 코드를 자동화된 절차로 배포 가능한 상태로 만들거나 운영 환경까지 배포하는 것이다. 사람이 수동으로 하던 빌드·테스트·배포를 자동화해 실수를 줄이고 배포 주기를 짧게 한다.
이렇게 바꿔 보세요
- 처음 설정으로 — 버전을 2.+ 로 적은 빌드. 코드를 바꾸지 않았는데 악성 버전이 들어옵니다.
- 잠금 파일을 두면 — 2.+ 라고 적어도 잠금 파일에 적힌 버전만 받습니다.
- 범위가 도움이 되는 경우 — 취약점을 고친 버전이 올라오면 자동으로 따라갑니다.
- 전이 의존성의 취약점 — 내가 적지 않은 json-lite에서 취약점이 알려졌습니다. 그대로 두면 남습니다.
- 버전 올리기 — web-kit을 올리면 json-lite도 함께 바뀝니다.
- 올릴 버전이 아직 없을 때 — 전이 의존성의 버전만 덮어씁니다.
- 이름을 한 글자 잘못 적으면 — 오류 없이 가짜 패키지가 들어옵니다.
처음 설정으로 실행하면
- 처음 빌드한 날입니다. build.gradle에 web-kit의 버전을 2.+ 라고 적었습니다. "2.x 버전 가운데 가장 높은 것"이라는 뜻입니다(2.0.0, 2.0.1, 2.1.0이 모두 2.x입니다). 지금 저장소에 올라와 있는 web-kit은 2.0.0 하나뿐이므로 2.0.0 버전이 들어오고, web-kit이 필요로 하는 json-lite 1.2.0도 함께 들어옵니다.
- 며칠 뒤 저장소에 web-kit 2.1.0이 올라옵니다. (꾸민 상황) 배포 계정을 빼앗은 누군가가 악성 코드를 넣어 올린 버전입니다. 내 코드가 아니라 라이브러리가 들어오는 길을 노린 공급망 공격입니다. 저장소에서는 평범한 새 버전으로 보입니다.
- 내 코드는 한 글자도 바꾸지 않고 다시 빌드합니다: 2.+ 에 맞는 버전을 저장소에서 다시 찾습니다. 2.0.0과 2.1.0 가운데 더 높은 2.1.0 버전을 고릅니다. 지난번과 다른 코드가 내 프로그램에 들어왔습니다.
- 빌드는 성공합니다. 오류도 경고도 없습니다. 악성 코드가 든 web-kit 2.1.0이 내 프로그램의 일부로 배포됩니다.
실행 결과
- 버전을 범위로 적으면 같은 코드를 빌드해도 날마다 결과가 달라질 수 있습니다. 고친 버전을 자동으로 받는 것도, 악성 버전을 자동으로 받는 것도 같은 성질에서 나옵니다. 무엇이 들어올지를 저장소에 올리는 사람이 정하게 됩니다.
코드
// build.gradle (내 프로젝트 shop-api)
dependencies {
implementation 'lab.demo:web-kit:2.+'
implementation 'lab.demo:mail-kit:1.4.0'
}
// lab.demo로 시작하는 라이브러리는 모두 설명을 위해 꾸민 것이다
이 모형의 전제
개념을 보이기 위한 모형이며, lab.demo로 시작하는 라이브러리와 그 버전, 취약점, 악성 버전, 가짜 패키지는 모두 꾸민 것입니다. 실제 라이브러리나 실제 취약점 번호와는 관계가 없습니다. 내용의 틀은 OWASP Top 10:2025의 A03(Software Supply Chain Failures, 소프트웨어 공급망 실패)이 다루는 범위를 따랐습니다: 직접 쓰는 것뿐 아니라 전이 의존성까지 버전을 파악할 것, 취약점 공지를 꾸준히 확인할 것, 믿을 수 있는 출처에서 받을 것. 버전이 정해지는 규칙은 Gradle을 기준으로 했고, 열다섯 가지 조합에서 어떤 버전이 들어오는지와 json-lite가 들어오는 길을, 꾸민 라이브러리의 설명 파일(POM)만 담은 이 PC 안의 저장소를 Gradle 8.14.3으로 실제 해석한 결과와 대조했습니다(네트워크 없이 실행). 실행으로 확인한 규칙: 2.+ 는 2.으로 시작하는 버전 가운데 가장 높은 것을 고른다, 잠금 파일이 있으면 거기 적힌 버전을 받는다, 한 라이브러리에 요구가 여럿이면 가장 높은 버전을 고른다, 없는 이름은 찾을 수 없다는 오류가 된다. 잠금 파일이 있는 채로 build.gradle의 버전만 고치면 잠금 파일과 맞지 않는다는 오류가 나므로 잠금 파일도 함께 갱신해야 한다는 것도 확인했습니다. 실행으로 확인하지 않은 것: "취약", "악성", "가짜"라는 판정은 꾸민 줄거리이고, 빌드에 들어온 코드가 실제로 실행되는 과정은 다루지 않았습니다. 버전 규칙은 도구마다 다릅니다. Maven은 요구가 여럿일 때 가장 높은 버전이 아니라 내 프로젝트에 가장 가까운 쪽의 버전을 고르고(Maven 문서의 의존성 중재), npm은 ^1.2.0처럼 범위를 기본으로 적고 package-lock.json으로 고정합니다. 이름이 비슷한 가짜 패키지는 이름만으로 패키지를 구분하는 저장소에서 주로 문제가 되며, Maven Central은 그룹 이름의 소유를 확인하므로 같은 그룹 아래에 가짜를 올리기는 어렵습니다. 여기서는 설명을 위해 같은 그룹에 둔 것으로 꾸몄습니다.
앞 실습: 요청 수 제한 · 다음 실습: 해시·암호화·서명