Denoのレジストリにおける任意パッケージの改竄 + encoding/yamlのCode Injection
(You can read this article in English too.)
免責事項
Denoを開発しているDeno Land Inc.は、脆弱性報奨金制度等を実施しておらず、脆弱性の診断行為に関する明示的な許可を出していません。
本記事は、公開されている情報を元に脆弱性の存在を推測し、実際に攻撃/検証することなく潜在的な脆弱性として報告した問題に関して説明したものであり、無許可の脆弱性診断行為を推奨することを意図したものではありません。
Deno Land Inc.が開発するサービスや製品に脆弱性を見つけた場合は、[email protected]へ報告してください。1
また、脆弱性に関する情報の検証が行えていないため、本記事に含まれる情報は不正確である可能性が存在します。2
要約
deno.land/xが動作しているシステム上の任意のファイルを読み取ることが可能な脆弱性、及びDenoのencoding/yaml
におけるCode Injectionを発見した。
このうち、deno.land/xの脆弱性が悪用された場合、モジュールをS3へ格納する際に使用されているAWSの認証情報を窃取することができたため、結果としてdeno.land/x上の任意のパッケージを改竄することが可能となっていた。
調査理由
Denoの公式サイトに記載されているDeno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.
という文言を読み、どれぐらいセキュアなのかが気になったため、調査を行うことにした。
調査範囲
調査を行うにあたって、関連するソフトウェアすべてを調査する時間はなかったため、以下のリポジトリに絞ってコードを流し読みすることにした。
- denoland/deno: Denoのコア部分
- denoland/deno_std: Denoの標準モジュール
- denoland/deno_registry2: deno.land/xのコード
調査結果
簡単な調査を行った結果、Deno本体、Denoの標準モジュール、deno.land/xのコードにそれぞれ脆弱性を見つけた。
そのうち、Deno本体に存在した脆弱性に関しては既知の問題だった3ため、本記事では紹介しないが、その他の2件に関しては以下で説明する。
Denoの標準モジュール
Denoには、本体とは別にDeno standard libraryというモジュールが存在する。
このモジュールは、Deno本体とは別のリポジトリで管理されており、Denoの本体には組み込まれていない。
このモジュール内に存在する、YAMLをパースするためのパッケージ(encoding/yaml
)に脆弱性が存在し、悪意あるYAMLを読み込んだ際に任意のコードを実行することが可能となっていた。
encoding/yamlの脆弱性 (CVE-2021-42139)
encoding/yaml
には、デフォルトで読み込まれているスキーマとは別に、拡張スキーマが存在する。
このスキーマは、以下のようなコードを書くことにより使用することができる。
import {
EXTENDED_SCHEMA,
parse,
} from "https://deno.land/std@$STD_VERSION/encoding/yaml.ts";
const data = parse(
`
regexp:
simple: !!js/regexp foobar
modifiers: !!js/regexp /foobar/mi
undefined: !!js/undefined ~
function: !!js/function >
function foobar() {
return 'hello world!';
}
`,
{ schema: EXTENDED_SCHEMA },
);
この拡張スキーマを使用することにより、正規表現やundefined、JavaScriptの関数をYAMLで表現することが可能になる。
これらの機能のうち、関数をパースする際の処理が以下のようになっていたため、単純なJavaScriptを記述することにより、任意のコードを実行することが可能となっていた。
function reconstructFunction(code: string) {
const func = new Function(`return ${code}`)();
if (!(func instanceof Function)) {
throw new TypeError(`Expected function but got ${typeof func}: ${code}`);
}
return func;
}
本脆弱性は、このプルリクエストにおいて、EXTENDED_SCHEMA
における関数のサポートを削除することにより修正された。
deno.land/x
deno.land/xは、Denoのモジュールをホストできるサービスで、GitHubのリリースと連動して自動でモジュールを更新することができる。
Denoのインストールスクリプトを含む多数のモジュールをホストしており、Denoのエコシステムの中心部分となっている。
GitHubと連携した自動更新機能は、デフォルトでリポジトリのすべてのファイルをdeno.land/xへアップロードする。
しかしながら、リポジトリ全体をアップロードしてしまうと余分なファイルも含まれてしまい、ファイルサイズが大きくなってしまう。
そこで、この問題に対応するために、アップロードするディレクトリを指定できるsubdir
パラメータが存在する。
このパラメータの値が適切にサニタイズされないままパスに連結されており、システム上の任意のファイルを読み取ることが可能となっていた。
subdirパラメータのパストラバーサル
subdir
パラメータは、以下のコードに到達するまで一切の変更を受けずにclonePath
へと連結される。
// Create path that has possible subdir prefix
const path = (subdir === undefined ? clonePath : join(
clonePath,
subdir.replace(
/(^\/|\/$)/g,
"",
),
));
ここで作成されたpath
は、以下のコードで用いられる。
// Walk all files in the repository (that start with the subdir if present)
const entries = [];
for await (
const entry of walk(path, {
includeFiles: true,
includeDirs: true,
})
) {
entries.push(entry);
}
console.log("Total files in repo", entries.length);
const directory: DirectoryListingFile[] = [];
await collectAsyncIterable(pooledMap(100, entries, async (entry) => {
const filename = entry.path.substring(path.length);
// If this is a file in the .git folder, ignore it
if (filename.startsWith("/.git/") || filename === "/.git") return;
if (entry.isFile) {
const stat = await Deno.stat(entry.path);
directory.push({ path: filename, size: stat.size, type: "file" });
} else {
directory.push({ path: filename, size: undefined, type: "dir" });
}
}));
このコードは、上記で生成したパスの配下に存在するファイルの情報をdirectory
という名前の配列に追加しており、このdirectory
は以下で用いられる。
await collectAsyncIterable(pooledMap(65, directory, async (entry) => {
if (entry.type === "file") {
const file = await Deno.open(join(path, entry.path));
const body = await Deno.readAll(file);
await uploadVersionRaw(
moduleName,
version,
entry.path,
body,
);
file.close();
}
}));
このコードでは、type
がfile
に設定されているファイル情報のpath
が指し示す場所に存在するファイルを読み取り、それをuploadVersionRaw
関数へ渡している。
この関数は、S3にpublic-read
の状態でファイルのコンテンツをアップロードする。
これらのコードからわかるように、subdir
パラメータに../../../../../../../etc
といったような値が設定されていた場合、/etc
ディレクトリ内のファイルが全てアップロードされる。4
これを用いることにより、悪意のあるユーザーがsubdir
パラメータに../../../../../../../proc/self
等を指定し、/proc/self/environ
からAWSの認証情報を摂取した上で任意のモジュールのファイルを書き換えることが可能となっていた。
本脆弱性は、このプルリクエストにおいてsubdir
パラメータを正規化するように変更することで修正された。
なお、本脆弱性の修正後にDenoの開発チームがAWS認証情報のアクセスログを調査し、本脆弱性が悪用されていないことを確認した。
まとめ
今回の記事では、Denoに関連したプロジェクトに存在した脆弱性に関して解説しました。
これらの事例からは、どんなにプロジェクトがセキュア
を掲げていても、脆弱性を作り込まないようにするというのは難しいということが分かるかと思います。
本記事に関する質問/感想はTwitter(@ryotkak)までお願いいたします。
タイムライン
日付 (日本時間) | 出来事 |
---|---|
2021/09/13 | 脆弱性の発見 |
2021/09/14 | 脆弱性の報告 |
2021/09/14 | 脆弱性が修正される |
2021/09/15 | 脆弱性の開示を行っても問題ないかを確認 |
2021/10/07 | 開示許可が出る |
2021/11/30 | 本記事の公開 |