[{"data":1,"prerenderedAt":680},["ShallowReactive",2],{"NoscriptNav_XrRK2e2e8meJ0jKVGkb5ULGQDVi3UiFQ9nupAr7Yns":3,"\u002Freports\u002Ftyposquatting-in-package-managers":8},["Island",4],{"key":5,"result":6},"NoscriptNav_XrRK2e2e8meJ0jKVGkb5ULGQDVi3UiFQ9nupAr7Yns",{"head":7},{},{"id":9,"title":10,"authors":11,"body":13,"canonicalUrl":666,"canonicalWebsiteName":667,"category":668,"date":669,"description":670,"extension":671,"featured":672,"fullWidthLayout":672,"image":673,"imageAlt":673,"location":673,"meta":674,"metaImage":673,"navigation":675,"path":676,"seo":677,"stem":678,"venue":673,"venueUrl":673,"__hash__":679},"reports\u002Freports\u002Ftyposquatting-in-package-managers.md","Typosquatting in Package Managers",[12],"andrew",{"type":14,"value":15,"toc":658},"minimark",[16,28,49,54,57,79,95,109,125,140,180,205,218,231,254,290,294,303,326,358,377,393,412,433,440,444,453,456,488,491,514,536,539,554,557,577,598,607,628,632,651,654],[17,18,19,20,27],"p",{},"Typosquatting is registering a package name that looks like a popular one, hoping developers mistype or copy-paste the wrong thing. It's been a supply chain attack vector since at least 2016, when Nikolai Tschacher ",[21,22,26],"a",{"href":23,"rel":24},"https:\u002F\u002Fincolumitas.com\u002F2016\u002F06\u002F08\u002Ftyposquatting-package-managers\u002F",[25],"nofollow","demonstrated"," that uploading malicious packages with slightly misspelled names could infect thousands of hosts within days. His bachelor thesis experiment infected over 17,000 machines across PyPI, npm, and RubyGems, with half running his code as administrator.",[17,29,30,31,35,36,39,40,43,44,48],{},"The attack surface is straightforward: package managers accept whatever name you type. If you run ",[32,33,34],"code",{},"pip install reqeusts"," instead of ",[32,37,38],{},"pip install requests",", and someone has registered ",[32,41,42],{},"reqeusts",", you get their code. The typo can come from your fingers, from a tutorial you copied, or from an LLM hallucination (",[21,45,47],{"href":46},"\u002Fideas\u002Fslopsquatting-meets-dependency-confusion","slopsquatting",").",[50,51,53],"h3",{"id":52},"generation-techniques","Generation techniques",[17,55,56],{},"There's a taxonomy of ways to generate plausible typosquats:",[17,58,59,63,64,67,68,71,72,71,75,78],{},[60,61,62],"strong",{},"Omission"," drops a single character. ",[32,65,66],{},"requests"," becomes ",[32,69,70],{},"reqests",", ",[32,73,74],{},"requsts",[32,76,77],{},"rquests",". These catch fast typists who miss keys or developers working from memory.",[17,80,81,84,85,67,87,90,91,94],{},[60,82,83],{},"Repetition"," doubles a character. ",[32,86,66],{},[32,88,89],{},"rrequests"," or ",[32,92,93],{},"requestss",". Easy to type accidentally, especially on phone keyboards.",[17,96,97,100,101,67,103,90,105,108],{},[60,98,99],{},"Transposition"," swaps adjacent characters. ",[32,102,66],{},[32,104,42],{},[32,106,107],{},"requsets",". This is probably the most common typing error.",[17,110,111,114,115,67,117,120,121,124],{},[60,112,113],{},"Replacement"," substitutes adjacent keyboard characters. ",[32,116,66],{},[32,118,119],{},"requezts"," (z is next to s) or ",[32,122,123],{},"requewts"," (w is next to e). Varies by keyboard layout.",[17,126,127,130,131,67,133,90,136,139],{},[60,128,129],{},"Addition"," inserts characters at the start or end (not mid-string). ",[32,132,66],{},[32,134,135],{},"arequests",[32,137,138],{},"requestsa",". Catches stray keypresses before or after the name.",[17,141,142,149,150,67,152,155,156,159,160,163,164,167,168,171,172,175,176,179],{},[60,143,144],{},[21,145,148],{"href":146,"rel":147},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHomoglyph",[25],"Homoglyph"," uses lookalike characters. ",[32,151,66],{},[32,153,154],{},"reque5ts"," (5 looks like s) or ",[32,157,158],{},"requεsts"," (Greek epsilon looks like e). In many fonts, ",[32,161,162],{},"l"," (lowercase L), ",[32,165,166],{},"1"," (one), and ",[32,169,170],{},"I"," (uppercase i) are nearly identical. The string ",[32,173,174],{},"Iodash"," (starting with uppercase i) displays identically to ",[32,177,178],{},"lodash"," (starting with lowercase L) in most terminals.",[17,181,182,185,186,67,189,90,192,195,196,71,198,200,201,204],{},[60,183,184],{},"Delimiter"," changes separators between words. ",[32,187,188],{},"my-package",[32,190,191],{},"my_package",[32,193,194],{},"mypackage",". Different registries normalize these differently: PyPI treats ",[32,197,188],{},[32,199,191],{},", and ",[32,202,203],{},"my.package"," as equivalent, but npm doesn't.",[17,206,207,210,211,67,214,217],{},[60,208,209],{},"Word order"," rearranges compound names. ",[32,212,213],{},"python-nmap",[32,215,216],{},"nmap-python",". Both sound reasonable, and developers might guess wrong.",[17,219,220,223,224,227,228,230],{},[60,221,222],{},"Plural"," adds or removes trailing s. ",[32,225,226],{},"request"," versus ",[32,229,66],{},". Both get registered, and tutorials using the wrong one send traffic to the wrong package.",[17,232,233,240,241,67,243,71,246,249,250,253],{},[60,234,235],{},[21,236,239],{"href":237,"rel":238},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCombosquatting",[25],"Combosquatting"," adds common suffixes. ",[32,242,178],{},[32,244,245],{},"lodash-js",[32,247,248],{},"lodash-utils",", or ",[32,251,252],{},"lodash-core",". These piggyback on brand recognition while looking like official extensions.",[17,255,256,257,260,261,263,264,267,268,275,276,263,279,282,283,286,287,48],{},"Less common techniques include ",[60,258,259],{},"vowel swaps"," (",[32,262,66],{}," to ",[32,265,266],{},"raquests","), ",[60,269,270],{},[21,271,274],{"href":272,"rel":273},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBitsquatting",[25],"bitsquatting"," (single-bit memory errors that change ",[32,277,278],{},"google",[32,280,281],{},"coogle","), and ",[60,284,285],{},"adjacent insertion"," (inserting a key next to one you pressed, like ",[32,288,289],{},"googhle",[50,291,293],{"id":292},"examples-from-the-wild","Examples from the wild",[17,295,296,297,302],{},"I've been collecting confirmed typosquats into a ",[21,298,301],{"href":299,"rel":300},"https:\u002F\u002Fgithub.com\u002Fecosyste-ms\u002Ftyposquatting-dataset",[25],"dataset",". It currently has 143 entries across PyPI, npm, crates.io, Go, and GitHub Actions, drawn from security research by OpenSSF, Datadog, IQTLabs, and others.",[17,304,305,306,311,312,314,315,317,318,321,322,325],{},"The existing malicious package databases are large. OpenSSF's ",[21,307,310],{"href":308,"rel":309},"https:\u002F\u002Fgithub.com\u002Fossf\u002Fmalicious-packages",[25],"malicious-packages"," repo has thousands of entries. Datadog's dataset has over 17,000. But most entries just list the malicious package name without identifying what it was targeting. A package called ",[32,313,42],{}," is obviously squatting ",[32,316,66],{},", but ",[32,319,320],{},"beautifulsoup-numpy"," could be targeting either library, and names like ",[32,323,324],{},"payments-core"," require context to understand. The dataset I built maps each malicious package to its intended target and classifies which technique was used. Inclusion requires a clear target: if I can't confidently say what package the attacker was imitating, it doesn't go in. That mapping is what you need to test detection tools: you can't measure recall without knowing what the attacks were trying to hit.",[17,327,328,329,331,332,71,334,71,337,71,339,71,341,71,344,71,346,71,348,71,350,71,352,200,354,357],{},"The ",[32,330,66],{}," library on PyPI has been targeted more than any other package. The dataset includes ",[32,333,42],{},[32,335,336],{},"requets",[32,338,77],{},[32,340,119],{},[32,342,343],{},"requeats",[32,345,135],{},[32,347,93],{},[32,349,89],{},[32,351,154],{},[32,353,266],{},[32,355,356],{},"requists",".",[17,359,360,361,364,365,368,369,372,373,376],{},"BeautifulSoup has ",[32,362,363],{},"beautifulsup4"," (omission), ",[32,366,367],{},"BeautifulSoop"," (replacement), ",[32,370,371],{},"BeaotifulSoup"," (transposition), and ",[32,374,375],{},"beautifulsoup-requests"," (combosquatting). The variations in capitalization are intentional: PyPI normalizes case, so attackers don't need to match it exactly.",[17,378,328,379,382,383,386,387,392],{},[32,380,381],{},"crossenv"," npm attack from 2017 exploited delimiter confusion with ",[32,384,385],{},"cross-env",", a popular build tool. Same words, different punctuation. ",[21,388,391],{"href":389,"rel":390},"https:\u002F\u002Fwww.bleepingcomputer.com\u002Fnews\u002Fsecurity\u002Fjavascript-packages-caught-stealing-environment-variables\u002F",[25],"Over 700 affected hosts"," downloaded the malicious version before it was caught.",[17,394,395,396,399,400,403,404,407,408,411],{},"Some attacks are creative. The packages ",[32,397,398],{},"--legacy-peer-deps"," and ",[32,401,402],{},"--no-audit"," on npm squat on CLI flag names. If someone copies ",[32,405,406],{},"npm install example--hierarchical"," from a tutorial with a missing space, npm parses ",[32,409,410],{},"--hierarchical"," as a package name to install rather than a flag.",[17,413,414,415,419,420,71,423,200,426,429,430,432],{},"GitHub Actions has its own variant. Orca Security ",[21,416,26],{"href":417,"rel":418},"https:\u002F\u002Forca.security\u002Fresources\u002Fblog\u002Ftyposquatting-in-github-actions\u002F",[25]," attacks on workflow files by registering organizations like ",[32,421,422],{},"actons",[32,424,425],{},"action",[32,427,428],{},"circelci",". They found 158 repositories already referencing a malicious ",[32,431,425],{}," org before they reported it.",[17,434,435,436,439],{},"Typosquatting also shows up in package metadata. A package's homepage or repository URL might point to a typosquatted domain, accidentally or deliberately. A maintainer who fat-fingers ",[32,437,438],{},"githb.com"," in their gemspec creates a link to someone else's server. An attacker who controls that domain gets traffic from anyone who clicks through from the registry page.",[50,441,443],{"id":442},"detection-tools","Detection tools",[17,445,446,447,452],{},"I've built a ",[21,448,451],{"href":449,"rel":450},"https:\u002F\u002Fgithub.com\u002Fandrew\u002Ftyposquatting",[25],"Ruby gem"," that generates typosquat variants and checks if they exist on registries. It supports PyPI, npm, RubyGems, Cargo, Go, Maven, NuGet, Composer, Hex, Pub, and GitHub Actions.",[17,454,455],{},"Generate variants for a package name:",[457,458,463],"pre",{"className":459,"code":460,"language":461,"meta":462,"style":462},"language-bash shiki shiki-themes github-light github-dark","typosquatting generate requests -e pypi\n","bash","",[32,464,465],{"__ignoreMap":462},[466,467,470,474,478,481,485],"span",{"class":468,"line":469},"line",1,[466,471,473],{"class":472},"sScJk","typosquatting",[466,475,477],{"class":476},"sZZnC"," generate",[466,479,480],{"class":476}," requests",[466,482,484],{"class":483},"sj4cs"," -e",[466,486,487],{"class":476}," pypi\n",[17,489,490],{},"Check which variants actually exist:",[457,492,494],{"className":459,"code":493,"language":461,"meta":462,"style":462},"typosquatting check lodash -e npm --existing-only\n",[32,495,496],{"__ignoreMap":462},[466,497,498,500,503,506,508,511],{"class":468,"line":469},[466,499,473],{"class":472},[466,501,502],{"class":476}," check",[466,504,505],{"class":476}," lodash",[466,507,484],{"class":483},[466,509,510],{"class":476}," npm",[466,512,513],{"class":483}," --existing-only\n",[17,515,516,517,522,523,525,526,71,529,200,532,535],{},"This queries the ",[21,518,521],{"href":519,"rel":520},"https:\u002F\u002Fpackages.ecosyste.ms",[25],"ecosyste.ms"," package names API. For ",[32,524,178],{},", it finds ",[32,527,528],{},"lodas",[32,530,531],{},"lodah",[32,533,534],{},"1odash"," already registered.",[17,537,538],{},"Scan an SBOM for potential typosquats in your dependencies:",[457,540,542],{"className":459,"code":541,"language":461,"meta":462,"style":462},"typosquatting sbom bom.json\n",[32,543,544],{"__ignoreMap":462},[466,545,546,548,551],{"class":468,"line":469},[466,547,473],{"class":472},[466,549,550],{"class":476}," sbom",[466,552,553],{"class":476}," bom.json\n",[17,555,556],{},"Check for dependency confusion risks on a package name:",[457,558,560],{"className":459,"code":559,"language":461,"meta":462,"style":462},"typosquatting confusion my-internal-package -e npm\n",[32,561,562],{"__ignoreMap":462},[466,563,564,566,569,572,574],{"class":468,"line":469},[466,565,473],{"class":472},[466,567,568],{"class":476}," confusion",[466,570,571],{"class":476}," my-internal-package",[466,573,484],{"class":483},[466,575,576],{"class":476}," npm\n",[17,578,579,580,585,586,591,592,597],{},"Other tools: the Rust Foundation maintains ",[21,581,584],{"href":582,"rel":583},"https:\u002F\u002Fgithub.com\u002Frustfoundation\u002Ftypomania",[25],"typomania",", which powers crates.io's typosquatting detection. IQTLabs built ",[21,587,590],{"href":588,"rel":589},"https:\u002F\u002Fgithub.com\u002FIQTLabs\u002Fpypi-scan",[25],"pypi-scan"," for PyPI (now archived). ",[21,593,596],{"href":594,"rel":595},"https:\u002F\u002Fgithub.com\u002Fmt3443\u002Ftypogard",[25],"typogard"," checks npm packages and their transitive dependencies.",[17,599,600,601,606],{},"SpellBound, a ",[21,602,605],{"href":603,"rel":604},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2003.03471",[25],"USENIX paper from 2020",", combined lexical similarity with download counts to flag packages that look like popular ones but have suspicious usage patterns. It achieved a 0.5% false positive rate and caught a real npm typosquat during evaluation.",[17,608,609,610,615,616,621,622,624,625,627],{},"The harder problem is preventing typosquats at registration time. PyPI ",[21,611,614],{"href":612,"rel":613},"https:\u002F\u002Fgithub.com\u002Fpypi\u002Fwarehouse\u002Fissues\u002F9527",[25],"discussed"," implementing \"social distancing\" rules that would block names too similar to popular packages. The analysis found that 18 of 40 historical typosquats had a ",[21,617,620],{"href":618,"rel":619},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLevenshtein_distance",[25],"Levenshtein distance"," of 2 or less from their targets, meaning one or two edits (a dropped letter, a swapped pair) was enough to create the attack name. Edit distance alone misses homoglyphs and keyboard-adjacent replacements, which is why detection tools need multiple techniques. But false positives are politically difficult: blocking ",[32,623,226],{}," because ",[32,626,66],{}," exists would annoy legitimate package authors.",[50,629,631],{"id":630},"the-friendly-typosquat","The friendly typosquat",[17,633,634,635,640,641,35,644,647,648,650],{},"Not all typosquats are malicious. Will Leinweber registered the gem ",[21,636,639],{"href":637,"rel":638},"https:\u002F\u002Frubygems.org\u002Fgems\u002Fbundle",[25],"bundle"," back in 2011. If you accidentally type ",[32,642,643],{},"gem install bundle",[32,645,646],{},"gem install bundler",", you get a package that does one thing: depend on bundler. The description says \"You really mean ",[32,649,646],{},". It's okay. I'll fix it for you this one last time...\"",[17,652,653],{},"It has 8 million downloads. That's 8 million typos caught and redirected to the right place. Defensive squatting like this is a public service.",[655,656,657],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":462,"searchDepth":659,"depth":659,"links":660},2,[661,663,664,665],{"id":52,"depth":662,"text":53},3,{"id":292,"depth":662,"text":293},{"id":442,"depth":662,"text":443},{"id":630,"depth":662,"text":631},"https:\u002F\u002Fnesbitt.io\u002F2025\u002F12\u002F17\u002Ftyposquatting-in-package-managers","nesbitt.io","software-supply-chains","2025-12-17","A reference guide to typosquatting techniques, real-world examples, and detection tools.","md",false,null,{},true,"\u002Freports\u002Ftyposquatting-in-package-managers",{"title":10,"description":670},"reports\u002Ftyposquatting-in-package-managers","aAPvfRr4INuDaVinxsNp58dxWtftpowbIx_cHkis19I",1780596102870]