Skip to main content

string-width-cjs npmパッケージに潜む、不可解なサプライチェーン上の懸念

著者
feature snyk supply chain purple

2024年10月3日

0 分で読めます

この話は、Docusaurus(Reactベースのオープンソースドキュメントプロジェクト)のメンテナーであるSébastien Lorberが、パッケージマニフェストを変更するプルリクエストに気づいたところから始まります。人気のcliui npmパッケージに対して提案された変更は、次のとおりです。

プルリクエスト内のnpmパッケージエイリアス

具体的には、見慣れない構文を使ったnpm依存関係の変更に注目してください。

  "dependencies": {
    "string-width": "^5.1.2",
    "string-width-cjs": "npm:string-width@^4.2.0",
    "strip-ansi": "^7.0.1",
    "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
    "wrap-ansi": "^8.1.0",
    "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"

通常、パッケージの値にはセマンティックバージョニングのバージョン範囲、あるいはGitやファイルベースのURLが指定されます。しかし、ここではnpm:という特殊なプレフィックス構文が使われています。これはどういう意味でしょうか?

npmパッケージのエイリアスとは?

npmパッケージマネージャーは、パッケージの解決ルールをカスタム定義できるエイリアス機能をサポートしています。そのため、コードやロックファイル内でそのパッケージが参照されると、エイリアスで指定された名前とバージョンに解決されます。

このプルリクエストで提案された変更の場合、string-width-cjsパッケージは、バージョン^4.2.0のstring-widthパッケージに解決されます。つまり、node_modulesディレクトリにはstring-width-cjsというエントリーが作成されますが、その中身はstring-width@^4.2.0のものになります。ロックファイル(package-lock.json)でも同様に動作します。

パッケージのエイリアス機能はnpmパッケージマネージャーの機能で、ESMとCJSの両方に対応する場合などに利用できます。

とはいえ、パッケージのエイリアス機能は悪用される可能性があります。2021年に公開された記事とセキュリティ情報開示で、Snyk AmbassadorのNishant Jainは、依存関係の取り違えとサプライチェーンセキュリティ上の懸念に関連して、パッケージエイリアスを使い、公式のnpmjsレジストリで依存関係の情報を誤認させられることを実証しました。

このプルリクエストは悪意のないもので、サプライチェーン攻撃のリスクもありませんでした。しかし、パッケージ名に対するSébastienの懸念から、潜在的なセキュリティリスクが明らかになりました。 

npmのロックファイルから悪意あるモジュールに関する不審な挙動を見つける

プルリクエストを調査するため、Sébastienはlockfile-lintを使用しました。このツールは、package-lock.jsonやyarn.lockなどのロックファイルをチェックし、改ざんの兆候がないかを確認します。元のnpmパッケージの代わりに悪意あるパッケージが注入されていないかを検出できます。

ツールを実行すると、次の警告が表示されました。

npx lockfile-lint --path package-lock.json --allowed-hosts yarn npm --validate-https --validate-package-names

detected resolved URL for package with a different name: string-width-cjs
    expected: string-width-cjs
    actual: string-width

detected resolved URL for package with a different name: strip-ansi-cjs
    expected: strip-ansi-cjs
    actual: strip-ansi

detected resolved URL for package with a different name: wrap-ansi-cjs
    expected: wrap-ansi-cjs
    actual: wrap-ansi

 ✖ Error: security issues detected!

免責事項:lockfile-lintは、ロックファイルに関するセキュリティ上の懸念を明らかにした2019年の私の投稿を受けて、私が開発したツールです。 npmのロックファイルが悪意あるモジュールの注入を見逃すセキュリティ上の盲点になり得る理由。

要警戒:npmで人気パッケージに酷似した名前

上記のlockfile-lintの結果を受け、Sébastienがnpmでこれらのパッケージ名を検索したところ、驚いたことに、いずれも公開npmレジストリに存在していました。

  • https://www.npmjs.com/package/string-width-cjs

  • https://www.npmjs.com/package/strip-ansi-cjs

  • https://www.npmjs.com/package/wrap-ansi-cjs

Sébastienは、これらのパッケージ名がnpmに存在するだけでなく、不審な特徴も備えていることに気づきました。公開ソースコードリポジトリに紐づいておらず、調べても実際のコードは入っていません。また、個人情報を一切伴わず匿名で公開されていました。

npmパッケージstrip-ansi-cjsを見ても、READMEもソースコードリポジトリもありません。しかし、多くの正規の人気パッケージが同じ動作を参照しています。

実際、このパッケージは人気があり、依存元(このパッケージに依存する他のパッケージ)は529件、週間ダウンロード数は7,274件です。

npm上の不審なパッケージ strip-ansi-cjs

strip-ansi-cjsのコードを確認すると、このパッケージにはpackage.jsonというパッケージマニフェストファイルが1つあるだけです。

何もしないパッケージが、なぜこれほど多くダウンロードされ、これほど多くの他のパッケージに依存されているのでしょうか?

strip-ansi-cjsパッケージにはソースコードがありません

これらのnpmパッケージの作成者を調べてみましょう。

3つのパッケージはすべてhimanshutester002が所有しており、いずれも昨年、機械的に付けられたようなバージョン番号で公開されていました。注目すべき点をいくつか挙げます。

  • isaacs-cliui npmパッケージは、Isaac自身によるcliuiプロジェクトのフォークと、同氏の名前空間にある正規のnpmパッケージ@isaacs/cliuiを狙った、タイポスクワッティングの可能性があります。

  • azure-sdk-for-net npmパッケージは、同じ名前のプライベートパッケージを攻撃するための依存関係混同キャンペーンを意図した可能性があります。

  • link-deep npmパッケージは、lodashなどのユーティリティパッケージでよく使われる機能名を占有しています。

npm上の不審なメンテナーが所有するnpmパッケージ

また、npmjsのhimanshutester002のユーザープロフィールページには、身元を特定できる情報がありません。

先ほど、npmパッケージstrip-ansi-cjsを使うパッケージが500件以上あることを確認しました。人気の高さを示す肯定的な兆候とも考えられます。実際に見てみましょう。

npmjsレジストリに依存するパッケージ

一覧に含まれていると、信頼できそうに見えるかもしれません。しかし、本当にそうでしょうか?
たとえば、clazz-transformerやreact-native-multiply、あるいはgh-monoproject-cliといった名前は正当に見えますが、実際はどうでしょうか?

こちらがreact-native-multiplyのnpmパッケージページです。

依存関係776件、インストールコマンド、使用例コード、週間ダウンロード数が表示されたreact-native-multiplyのnpmパッケージページ。

このパッケージのダウンロード数はほぼなく、作成者は身元を特定できない匿名のnpmユーザーです。パッケージがリダイレクトするソースURLのリポジトリ、https://github[.]com/hasandader/react-native-multiplyは存在しません。GitHubのユーザープロフィールも非常に不審で、実質的な活動が見られません。

npmパッケージにはソースコードが含まれているように見えますが、詳しく調べると、「Hello World」アプリのプロトタイプ用に生成されたコードサンプルでした。

Android、C++、iOS、library、sourceの各フォルダーとパッケージファイルが表示された、/react-native-multiply/プロジェクトディレクトリのファイルブラウザー。

このパッケージが単なる乗算ライブラリだとしたら、次の処理をするのに776もの依存関係が必要なのか、疑問に思うはずです。

import { multiply } from 'react-native-multiply';
const result = await multiply(3, 7);

JavaScriptでは過剰な依存関係によって、ネストされたパッケージの依存ツリーが天文学的な規模になると冗談めかして言われることもあります。しかし、直接依存が776件というプロジェクトは、あまりにも巨大です。

これらの依存関係には、この話の発端となった不審なnpmパッケージ3つ、string-width-cjs, strip-ansi-cjsとwrap-ansi-cjsも含まれています。

関連する文字列ユーティリティの依存関係の中に、string-width-cjs npmパッケージのバージョン^5.1.1が表示されたコード一覧

strip-ansi-cjsの依存パッケージの1つがclazz-transformerという名前だとお伝えしました。詳しく見てみましょう。

README、インストールコマンド、依存関係、バージョン、ライセンス、ダウンロード数が表示されたclass-transformerのnpmパッケージページ。

ここで何が起きているのか説明しましょう。​​npmパッケージclazz-transformerは、READMEページ上で意図的にclass-transformerというタイトルを付けています。さらに、ソースコードリポジトリのhttps://github[.]com/typestack/class-transformerはパッケージ名と一致せず、正当性に疑問が生じます。

関連リポジトリであるGitHub上のtypstack/class-transformerでは、package.jsonファイルは次のようになっています。

class-transformerのJSONパッケージマニフェストを表示したダークテーマのコードエディター。バージョン0.5.1、リポジトリ、モジュールの詳細が記載されています

GitHub上のpackage.jsonファイルには依存関係の宣言がありません。しかし、npmjsにある実際のパッケージのソースコードを確認すると、このclazz-transformerには437件の依存関係が同梱されています。ここでも都合よく、不審な*-cjsパッケージ3つが含まれています。

clazz-transformerのJSON依存関係リストを示すnpmパッケージのコードビュー。パッケージ名とバージョン範囲を含む

不審なnpmパッケージの調査から考えられること

さらに結論を導き出す前に、上記のnpmパッケージで確認された特徴をいくつか挙げておきます。

  • React Nativeのパッケージは、create-react-native-libraryのスキャフォールドツールから派生したようです。このツールは、新規プロジェクト向けに生成される標準のソースコードに、デフォルトのmultiply関数のサンプルも含んでいます。

  • パッケージのディレクトリ構成やファイル構成、依存関係は、Next.js 14のスターターボイラープレートに由来する可能性があります。たとえば、npx create-next-app@14で作成されるものです。

Sonatypeの同業者は以前にも、オープンソースレジストリにパッケージを大量登録する同様の事例を特定しています。これらの事例では、最終的な目的は開発者がWeb3プラットフォームのTeaトークンを獲得することでした。Teaはオープンソースソフトウェアの収益化を目的としています。

言及したパッケージからtea.yamlファイルが見つかったことも、Teaの悪用を通じてTeaトークンをマイニングすることが、このキャンペーンの目的の一部であるという見方を裏付けています。

今年の2024年4月14日、Teaフォーラムのユーザーがコメントを投稿し、Teaの悪用に関する懸念をさらに裏付けました。

Teaの不正利用フォーラムのコメント — ほとんどのプロジェクトは役に立たないスパムだ

結論を述べる前に、慎重なメンテナーとしての姿勢を示し、npmサプライチェーン攻撃の可能性を示す一連の事象の発見に協力してくれたSébastien Lorberに、心から感謝します。

string-width-cjsで何が起きているのか?

現時点では、string-width-cjsに依存しているとされる残りのパッケージも調べれば、正当性が極めて疑わしい兆候を見つけられるという確信があります。

私の見立てでは、こうした依存パッケージやダウンロード数の水増しは、*-cjsパッケージ3つを正規のものに見せかけることだけが目的です。適切な標的が現れれば、これらの偽パッケージがインストールされ、その後、悪意ある新バージョンが投入されるでしょう。

オープンソースソフトウェアを安全に利用するため、セキュリティ対策の導入を強くおすすめします。特に、以下の学習リソースをご活用ください。

不正な行為の中からサプライチェーン攻撃のキャンペーンを突き止めたのでしょうか。それとも、目的は金銭的な利益であり、npmやGitHubのような公開レジストリを悪用したスパムや不正行為として、Teaトークンをマイニングしていたのでしょうか?

どのような展開になろうとも、警戒を怠らないでください。

開発しながらコードを安全に

Snykはコードの品質とセキュリティ上の問題をスキャンし、IDE上で修正方法を提案します。