Skip to main content

Node.jsで安全でないデシリアライゼーションを防ぐ

blog feature playwright tests

2023年4月17日

0 分で読めます

編集者注:2023年5月31日

この記事を更新し、Snyk Open SourceとSnyk VS Code拡張機能を使って、プロジェクトで安全でないデシリアライゼーションを防ぐ方法を紹介しています。

シリアライゼーションとは、JavaScriptオブジェクトを、ネットワーク経由で送信したりデータベースに保存したりできる連続したバイト列に変換するプロセスです。シリアライゼーションでは、元のデータ形式を変更しながら状態やプロパティを保持するため、必要なときに元の状態を再現できます。シリアライゼーションを使うと、複雑なデータをファイル、データベース、プロセス間メモリに書き込んだり、コンポーネント間やネットワーク経由で送信したりできます。

デシリアライゼーションはシリアライゼーションの逆で、シリアライズされたデータを元と同じ状態のオブジェクトに戻します。問題は、デシリアライゼーションが安全でない場合、機密データが漏えいするおそれがあることです。安全でないデシリアライゼーションの主な原因は、ユーザー入力のデシリアライゼーションを保護できていないことです。これを防ぐ最善の方法は、検証およびサニタイズ済みのデータのみをデシリアライズすることです。

攻撃者は、一見無害なシリアライズ済みオブジェクトに悪意のあるコードを仕込み、アプリケーションに送信することで、この脆弱性を悪用できます。追加のセキュリティチェックがなければ、Webアプリケーションは受信したオブジェクトをデシリアライズし、悪意のあるコードを実行してしまいます。また、攻撃者はシリアライズ済みオブジェクトに含まれる機密データを抜き取ることもできます。

安全でないシリアライゼーションは、深刻な結果を招く可能性があります。たとえば、Webサイトが安全でない方法でデータをデシリアライズすると、ハッカーはシリアライズ済みオブジェクトを改ざんし、有害なデータをアプリケーションのコードに渡すことができます。攻撃者は、まったく別のシリアライズ済みオブジェクトを渡してアプリケーションの他の部分にアクセスすることも可能です。その結果、アプリケーションはさらなる攻撃やデータ損失の危険にさらされます。

Node.jsにおける安全でないデシリアライゼーション

node-serializeとserialize-to-jsは、安全でないデシリアライゼーションの脆弱性があるNode.jsおよびJavaScriptのパッケージです。JSON形式のオブジェクトのみをシリアライズするJSON.parseやJSON.stringifyとは異なり、この2つのライブラリは関数を含むほぼあらゆる種類のオブジェクトをシリアライズできます。この特性により、オブジェクトのプロパティのデフォルト値を攻撃者が操作できるインジェクション攻撃、プロトタイプ汚染に対して脆弱になります。

プロトタイプ汚染攻撃が成功すると、攻撃者はアプリケーションのロジックを変更したり、リモートコード実行を引き起こしたり、サービス拒否(DoS)攻撃を行ったりして、さらなる被害をもたらす可能性があります。

この記事では、Node.jsのデシリアライゼーションの脆弱性を修正する方法を紹介します。脆弱なコードを作成し、攻撃を実演したうえで、脆弱性を修正します。

前提条件

このチュートリアルを進めるには、次のものが必要です。

  • Node.jsがインストールされていること

  • JavaScriptの知識

Node.jsのデシリアライゼーションの脆弱性を修正する

まず、Node.jsプロジェクトをセットアップしましょう。次のコマンドを実行します。

npm init -y

Express Webアプリケーションフレームワークをインストールします。

npm install express

Node.jsのシリアライゼーションパッケージをインストールします。

npm install node-serialize

Cookieパーサーをインストールします。

npm install cookie-parser

新しいファイル server.jsを作成し、以下のコードを追加します。

var express = require('express');
var cookieParser = require('cookie-parser');
var escape = require('escape-html');
var serialize = require('node-serialize');
var app = express();
app.use(cookieParser())

app.get('/', function(req, res) {
	if (req.cookies.profile) {
		var str = new Buffer(req.cookies.profile, 'base64').toString();
		var obj = serialize.unserialize(str);
		if (obj.username) {
			res.send("Hello " + escape(obj.username));
		}
	} else {
		res.cookie('profile', "eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9", {
			maxAge: 900000,
			httpOnly: true
		});
	}
	res.send("Welcome to the Serialize-Deserialize Demo!");
});
app.listen(3000);
console.log("Listening on port 3000...");

上のファイルでは、安全でないモジュールnode-serializeを使用しています。また、信頼できないデータをその関数unserialize()に渡しています。

上記のコードには、セキュリティ上の問題がいくつあるでしょうか。コードやプロジェクトに追加した依存関係にセキュリティ上の問題があるかどうか、どのように確認しますか。

そこで役立つのがSnyk Codeの拡張機能です。無料で使え、すぐに利用を始められます。VS Codeの拡張機能(SHIFT+CMD+X)を開き、Snykを検索してインストールし、90,000人を超える開発者の仲間に加わりましょう。

先ほどのExpressアプリケーションのコードスニペットで、Snyk Codeがどのように動作するかをスクリーンショットでお見せします。

Expressアプリケーションサーバーで発生し得る、複数のセキュリティ上の問題を示したスクリーンショット。

紫色の矢印が、このExpressアプリケーションサーバーに潜む潜在的なセキュリティ上の問題をどのように示しているかご覧ください。よく見ると、ほかの問題も見つかるでしょう。また、Snyk Codeが`server.js`ファイルのコード行に注釈を付け、異なるコードパスやコードファイル間で安全でないコードがどのように流れるかを示していることにも気づくはずです。

Snyk VS Code拡張機能を入手しましょう。無料でご利用いただけます。

Node.js Webアプリケーションに戻ると、app.get('/', function(req, res)エンドポイントが安全でないデシリアライゼーションに対して脆弱になっています。これは、すでにシリアライズ済みの設定Cookie値に、アプリケーションがユーザー入力を追加しているためです。

脆弱性を実演するため、次のコマンドを実行してExpressサーバーを起動します。

node server.js

これにより、ポート3000でサーバーが起動します。http://localhost:3000/を開いてページを表示し、以下のようにCookieを確認します。

ブラウザーの開発者ツールの[Application]タブに、localhostの「profile2」というCookieと、そのエンコードされた値が表示されています。

次に、CookieをBase64形式からJSON形式に変換します。Code Beautifyなどのオンラインツールを使えます。以下はBase64形式のCookieです。

eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9

Base64からJSONに変換すると、サーバーが設定したCookieは次のようになります。

{"username":"John","gender":"Male","Age": 35}

デシリアライゼーションの問題を実際に確認するため、JSONオブジェクトの値を変更してBase64にエンコードし、ブラウザーのCookie値を新しいCookieに置き換えましょう。

JSONオブジェクトを次のように変更します。

{"username":"Joe Jones","gender":"Male","Age": 40}

このBase64ツールを開いてJSONをエンコードします。Base64にエンコードすると、次のようになります。

eyJ1c2VybmFtZSI6IkpvZSBKb25lcyIsImdlbmRlciI6Ik1hbGUiLCJBZ2UiOiA0MH0=

次に、ブラウザーの現在のCookie値を新しいオブジェクトのエンコード済みの値に置き換え、server.jsファイルのHTTPレスポンス値を以下のように編集します。

res.send("Welcome to the Insecure Deserialize Demo!");

接続を再起動してページを更新すると、新しいレスポンスが表示されます。

脆弱性を悪用する

信頼できないデータをunserialize()関数に渡し、任意コード実行(ACE)を行って脆弱性を悪用してみましょう。

まず、作業中のものと同じモジュールのserialize()関数を使ってペイロードを作成します。そのために、serialize.jsという名前のファイルを作成し、以下のコードを追加します。

var serialize = require('node-serialize');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}
console.log("Serialized: \n" + serialize.serialize(m) + "\n");

これは、コードをシリアライズするNode.jsスクリプトです。

次に、ターミナルを開いてルートフォルダーに移動し、次のコマンドを実行します。

node serialize.js

Cookie値のusernameパラメーターを、上記の出力にあるmyOutputパラメーターの値に置き換えます。Cookie内の新しいオブジェクトは、次のようになります。

{"username":"_$$ND_FUNC$$_function(){ return 'Hello'; }","gender":"Male","Age": 40}

上記の出力から、unserialize()関数に渡すシリアライズ済みペイロードが得られます。ACEを実現するには、JavaScriptの即時実行関数式(IIFE)を使って関数を呼び出す必要があります。

このシリアライズ済みペイロードの関数本体の後ろにIIFEの括弧()を追加すると、オブジェクトの作成後に関数が実行されます。以下は、Cookieに保存されるJSONオブジェクトです。

{
	"username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
	"gender": "Male",
	"Age": 40
};

このように改ざんしたオブジェクトによって、実行中のNode.jsサーバーで動的なコード評価が行われることを示すため、ユーティリティファイルserialize.jsで例を実行します。このペイロードをunserialize()関数に渡しましょう。seralize.jsファイルを変更し、以下のコードを追加します。

var serialize = require('node-serialize');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}

console.log("Serialized: \n" + serialize.serialize(m) + "\n");

var km = {
	"username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
	"gender": "Male",
	"Age": 40
};
console.log(serialize.unserialize(km));

上記のコードでは、kmという新しい変数を追加しました。この変数には、usernameの値として関数が含まれており、それをunserialize()関数に渡しています。

次のコマンドを実行してユーティリティスクリプトファイルを実行し、関数が実行されるか確認します。

node serialize.js

次のような出力が得られます。

シリアライズされたJavaScriptデータと、ユーザー名、性別、年齢の各フィールドを含むデシリアライズ後の出力を示すコード例。

シリアライゼーションのプロセスが悪用されました。クライアント側オブジェクトへの入力を制御できる攻撃者として、JavaScriptコードを含むようにオブジェクトを改ざんし、usernameフィールドを変更できました。

安全でないデシリアライゼーションの脆弱性を修正する

安全でないデシリアライゼーションを防ぐ最善の方法は、ユーザー入力を一切デシリアライズしないことです。次善の方法は、ユーザー入力をシリアライズする前にチェックすることです。上記の脆弱な例では、Serialize JavaScriptというパッケージを使って入力をサニタイズできます。

まず、npmを使ってserialize-javaScriptをインストールします。

npm install serialize-javascript

serialize.jsファイルのコードを以下に置き換えます。

var serialize = require('serialize-javascript');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}
console.log("Serialized: \n" + serialize(m, {
	ignoreFunction: true
}) + "\n");

ignoreFunctionにより、ACEの実行に使われる関数がシリアライズされなくなります。ファイルを実行して確認しましょう。

node serialize.js

出力は次のとおりです。

「Serialized:」に続いて空のオブジェクト「{}」が表示されたコードスニペット。

この記事で推奨する新しいserialize-javascriptに、CVEレポートで報告されている脆弱性がないことを確認しましょう。以下のnpm lsコマンドを見ると、脆弱なnode-serializeパッケージとともに、このパッケージがプロジェクトに追加されていることがわかります。

$ npm ls
b@1.0.0 /private/tmp/b
├── cookie-parser@1.4.6
├── express@4.18.2
├── node-serialize@0.0.4
└── serialize-javascript@6.0.1

次に、無料のsnyk npmパッケージでスキャンします。

$ snyk test

Testing /private/tmp/b...

Tested 62 dependencies for known issues, found 1 issue, 1 vulnerable path.

Issues with no direct upgrade or patch:
  ✗ Arbitrary Code Execution [Critical Severity][https://security.snyk.io/vuln/npm:node-serialize:20170208] in node-serialize@0.0.4
    introduced by node-serialize@0.0.4
  No upgrade or patch available

Organization:      lirantal
Package manager:   npm
Target file:       package-lock.json

この記事で実証したとおり、node-serialize npmパッケージには脆弱性があることがわかります。一方、推奨するserialize-javascript npmパッケージには、公開されているセキュリティ脆弱性がありません。

Snyk Open Sourceを使い始めることを強くおすすめします。また、日常の開発ワークフローの一環としてsnyk npmパッケージを活用してください。新たなセキュリティ脆弱性が入り込まないよう、CI/CDパイプラインにも必ず追加しましょう。

デシリアライゼーションの脆弱性を防ぐためのベストプラクティス

ユーザー入力をデシリアライズすると、悪意のある攻撃者がシステムを攻撃し、機密データを漏えいさせるおそれがあります。安全でないデシリアライゼーションから保護する最善の方法は、信頼できないソースからのデータをデシリアライズしないことです。ただし、データをデシリアライズする必要がある場合は、データが改ざんされていないことを確認するための追加のセキュリティ対策を実装しましょう。

さらに、入力をサニタイズする必要があります。シリアライズするオブジェクトに関数が含まれているかを確認し、含まれている場合はシリアライゼーションを中止することで実現できます。

安全なデシリアライゼーション

シリアライゼーションとデシリアライゼーションは、ネットワーク経由でデータをスムーズに転送するために不可欠なプロセスです。しかし、安全でないデシリアライゼーションは深刻な脆弱性につながるおそれがあります。攻撃者は、一般にデシリアライズされるユーザー入力データとして、悪意のあるシリアライズ済みオブジェクトをWebアプリに注入し、アプリケーションに有害なデータを渡すことができます。その後、インジェクション攻撃やリモートコード実行、DDoS攻撃を仕掛ける可能性があります。

この脆弱性による攻撃は、ユーザー入力をデシリアライズしないだけで防止できます。デシリアライゼーションが必要な場合は、偽造防止トークンなどの追加のセキュリティ機構を組み込み、データが改変されていないことを確認しましょう。

Javaにおける安全でないシリアライゼーションおよびデシリアライゼーションの脆弱性を防ぐ方法について、Snykのブログで詳しくご覧ください。

安全でないシリアライゼーションおよびデシリアライゼーションの脆弱性からコードを保護するには、IDEにSnyk.ioプラグインを追加してください。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。