Dit projekt afhænger af et internt bibliotek — noget i retning af acme-billing-utils — udgivet til et privat registry. Ingen uden for virksomheden har nogensinde set det. Det navn er stadig ledigt for hvem som helst på det offentlige registry, og deri består hele angrebet.
Det er en af fem veje ind i en software-forsyningskæde, og blandt de nemmeste at lukke, når man først kan se den. Mekanismen er en opløsningsregel, ikke en fejl i et bestemt værktøj. De fleste pakkeværktøjer kan konfigureres med mere end én kilde: et privat registry til interne pakker og det offentlige til alt andet. Når en pakke bliver efterspurgt, tjekker de de tilgængelige kilder — og i en række almindelige opsætninger vinder et højere versionsnummer, uanset hvilken kilde det kom fra.
Så en angriber, der får kendskab til navnet acme-billing-utils, udgiver en pakke med det navn til det offentlige registry, med en version som 99.0.0. Dit næste build opløser det interne navn, ser en langt højere version tilgængelig offentligt og installerer angriberens kode — som typisk kører ved installation, på en build-server, med adgangsoplysninger i miljøet.
Intet blev kompromitteret for at få det til at virke. Ingen adgangskode blev stjålet. Angriberen havde kun brug for et navn.
Teknikken blev demonstreret offentligt af Alex Birsan i februar 2021, mod Apple, Microsoft, PayPal og snesevis af andre, med intet andet end pakkenavne samlet fra offentlige kilder. Hvert eneste af de builds opløste den højere version fra det offentlige registry.
Python fortjener en særlig bemærkning, for fælden ligger i et flag, folk kopierer uden at tænke over det. pip install --extra-index-url tilføjer et index uden at give det lavere prioritet: pip spørger begge og tager den højeste version, hvilket er præcis den sårbare adfærd. --index-url erstatter standarden i stedet og er den sikre form.
Hvor navnene lækker fra
Det er værd at være ærlig om, at interne pakkenavne ikke er hemmelige i praksis, selv når alle går ud fra det. De dukker op i:
- Committede lockfiles og manifester i et repository, der senere bliver offentligt — eller som altid har været det, fordi kun noget af virksomhedens kode er lukket.
- Bundlede frontend-filer, hvor et source map eller et uminificeret build sender navnene på interne moduler ud til hver eneste besøgende.
- Offentlige CI-logs — build-output på et offentligt repository udskriver de pakker, der bliver installeret.
- Stillingsopslag, konferenceoplæg og supportfora, hvor et stack trace bliver indsat ordret.
Den praktiske konklusion er, at et forsvar bygget på, at navnet forbliver privat, ikke er et forsvar.
Sådan opdager du eksponeringen i dit eget projekt
De fleste beskrivelser af angrebet vender detektionen på hovedet. De fremstiller det som noget, man kun kan tjekke, når man har opremset sine interne pakkenavne, hvilket får det til at lyde som en revision, der kræver opsætning og konfiguration.
Det gør det ikke. Tjekket er enklere og virker udefra: tag hver direkte afhængighed, dit projekt erklærer, og spørg det offentlige registry, om det navn findes.
En afhængighed, dit build kræver, hvis navn giver "findes ikke" fra sit offentlige registry, er per definition et navn, der bliver opløst et andet sted fra — et internt registry, en lokal sti, et spejl. Det ledige offentlige navn er eksponeringen, uanset hvem der ejer det. En angriber kan registrere det, og det eneste, der står mellem dig og deres kode, er din opløsningskonfiguration.
To forfininger gør resultatet langt mere brugbart:
Et privat registry konfigureret i repositoryet gør fundet mere sikkert. Indeholder projektet en .npmrc, der peger på et internt registry, en Maven-settings-fil med et eget repository eller lignende, så er forudsætningen for angrebet påviseligt til stede og ikke hypotetisk.
Workspaces er den uskyldige forklaring. I et monorepo refererer pakker til hinanden ved navn og bliver opløst lokalt — de giver også 404 offentligt, og de er helt i orden. At genkende en workspace-opsætning og sige det gør et forvirrende fund til et indlysende.
Sådan lukker du det reelt
Tre forsvar, i faldende orden efter hvor godt de virker:
Brug et scope eller navnerum, du selv ejer. På npm betyder det at udgive under et organisations-scope som @acme/billing-utils og eje @acme offentligt. En angriber kan ikke udgive ind i et scope, du kontrollerer. Det er den stærkeste løsning, fordi den fjerner tvetydigheden i stedet for at administrere den.
Tag navnene defensivt. Udgiv pladsholder-pakker under jeres interne navne på det offentlige registry. Groft, effektivt og gratis.
Konfigurér opløsningen eksplicit. Bind interne navne til det interne registry, så det offentlige aldrig bliver spurgt om dem. Det virker, men det afhænger af, at konfigurationen forbliver korrekt på tværs af hvert projekt og hver ny udviklers maskine — hvilket er præcis den slags, der skrider.
Uanset hvad I vælger, er den brugbare holdning at gå ud fra, at navnene allerede er kendte, og gøre navnet utilstrækkeligt.
Hvad er dependency confusion?
Dependency confusion er et forsyningskædeangreb, hvor en angriber udgiver en pakke til et offentligt registry med navnet på en af jeres interne, private pakker. Fordi mange build-opsætninger spørger både en privat og en offentlig kilde og foretrækker det højeste versionsnummer, kan jeres build opløse angriberens pakke i stedet for jeres og køre deres kode — typisk ved installation, på en build-server. Der skal ikke stjæles adgangsoplysninger; angriberen skal kun kende navnet.
Hvordan ved jeg, om mit projekt er eksponeret for dependency confusion?
Tag hver direkte afhængighed, dit projekt erklærer, og spørg det offentlige registry, om navnet findes. En afhængighed, dit build kræver, hvis navn giver findes-ikke fra sit offentlige registry, bliver opløst et andet sted fra — hvilket betyder, at det offentlige navn er ledigt, og at en angriber kunne registrere det. Det ledige navn er eksponeringen. Et privat registry konfigureret i repositoryet gør det mere alvorligt, mens en monorepo-workspace-opsætning er den almindelige uskyldige forklaring.
Hvordan forhindrer jeg dependency confusion?
Den stærkeste løsning er at udgive interne pakker under et navnerum eller scope, I selv ejer offentligt, for eksempel et npm-organisations-scope, for en angriber kan ikke udgive ind i et scope, I kontrollerer. At registrere pladsholder-pakker defensivt under jeres interne navne på det offentlige registry virker også og koster ingenting. At konfigurere pakkeværktøjet til kun at opløse interne navne fra det interne registry virker ligeledes, men afhænger af, at konfigurationen forbliver korrekt på tværs af hvert projekt og hver maskine.
Er interne pakkenavne egentlig hemmelige?
Interne pakkenavne er sjældent hemmelige i praksis. De lækker gennem committede lockfiles i repositories, der senere bliver offentlige, gennem source maps og uminificerede frontend-bundles sendt ud til hver besøgende, gennem build-output i offentlige CI-logs og gennem stack traces indsat i supportfora eller konferenceoplæg. Ethvert forsvar, der afhænger af, at navnet forbliver ukendt, bør betragtes som intet forsvar overhovedet.
Hvor CodeControl passer ind
CodeControl tjekker hver direkte afhængighed mod dens offentlige registry og rejser et fund for hvert navn, der ikke findes der — uden konfiguration eller opsætning, fordi det ledige navn er eksponeringen. Et privat registry konfigureret i repositoryet hæver alvorligheden, og npm-workspaces bliver genkendt, så den uskyldige monorepo-forklaring bliver tilbudt og ikke begravet. Det kører side om side med tjek for typosquatting og detektion af AI-opfundne pakkenavne.