Flutterのセキュリティ:リバースエンジニアリングと解析
Flutterアプリケーションをリバースエンジニアリングするための静的解析と動的解析の手法を解説する記事です。実行時のパッチ適用、独自ABIでの関数呼び出しのインターセプト、通信のインターセプトを取り上げます。
はじめに
Googleが開発したFlutterは、モバイルアプリ開発で広く使われているクロスプラットフォームのフレームワークで、Webアプリケーションやデスクトップアプリケーションにも対応しています。Flutterは大きく成長しており、AndroidとiOSのマーケットプレイスでそれぞれ340%と270%の増加を記録しています。
このフレームワークは、Skiaレンダリングエンジンによる高いパフォーマンスで知られています。また、複雑なUIデザインに対応できる柔軟なUIシステムも備えています。
Flutterは、既存のアプリケーションに組み込むことも、アプリケーションをゼロから設計することもできます。
このプラットフォームには強固で活発なコミュニティがあり、それが急速な成長を支えています。一方で、比較的新しいことから、Flutterはセキュリティの観点ではまだ十分に研究されておらず、モバイルアプリケーションのセキュリティ解析に関する取り組みも限られています。
Flutterは人気がありますが、セキュリティリスクと無縁ではありません。そのため、より優れたツールとフレームワークに関する知識が求められます。これには、Flutterアプリケーションを分解して調べ、潜在的なセキュリティ上の問題を見つけて理解する方法を学ぶことも含まれます。
ツールと手法
Flutterアプリケーションを解析するためのツールには、静的解析ツールと動的解析ツールの2つのカテゴリがあります。
Doldrumsなどの静的解析ツールは、ファイル形式を再実装することでファイルをパースします。Doldrumsは古いバージョンのFlutter(2.10と2.12。本記事の執筆時点での最新リリースは3.10)を部分的にしかサポートしておらず、ランタイムが急速かつ継続的に進化することに伴う課題から、すでにメンテナンスされていません。
一方、reFlutterのような動的解析ツールは、Flutterのランタイムにパッチを当てて実行中に情報を収集します。こうしたツールでは、動的に特定した関数を計装するために、Fridaのような補助ユーティリティやLLDBのようなデバッガーが必要になることがよくあります。
reFlutterはFlutterアプリケーションを解析する強力なアプローチを備えていますが、最新バージョンのランタイムにはもう対応していません。Flutterランタイムの変更により、パッチが動作しなくなっています。
Flutterアプリケーションの構造
Flutterアプリケーションは、どのプラットフォームでも、ラッパーアプリケーション、Flutterランタイム、Flutterアプリで構成されています。
以下はLinuxアプリケーションのレイアウトの例です。libapp.soがアプリケーションのコード、counterがラッパー、libflutter_linux_gtk.soがランタイムです。
├── counter
├── data
│ ├── flutter_assets
│ │ ├── AssetManifest.json
│ │ ├── FontManifest.json
│ │ ├── fonts
│ │ │ └── MaterialIcons-Regular.otf
│ │ ├── NOTICES.Z
│ │ ├── packages
│ │ │ └── cupertino_icons
│ │ │ └── assets
│ │ │ └── CupertinoIcons.ttf
│ │ ├── shaders
│ │ │ └── ink_sparkle.frag
│ │ └── version.json
│ └── icudtl.dat
└── lib
├── libapp.so
└── libflutter_linux_gtk.so
ラッパーアプリケーションはエントリーポイントを提供し、描画サーフェス、アクセシビリティ、入力といったサービスにアクセスするために基盤のオペレーティングシステムと連携し、メッセージのイベントループを管理します。
Flutterランタイムは、Flutterアプリの読み込みを担当し、アプリケーションに一連の機能を提供します。ランタイムはDart仮想マシン(VM)とFlutterエンジンで構成されており、後者は主にC++で書かれ、すべてのFlutterアプリケーションに必要なプリミティブをサポートしています。
以下は、Flutterランタイムが公開している関数の一覧です。
000000000041a860 g DF .text 00000000000001e8 Base fl_binary_messenger_send_response
000000000041f740 g DF .text 0000000000000043 Base fl_json_message_codec_new
000000000042a1f0 g DF .text 000000000000015c Base fl_method_channel_invoke_method
000000000042a390 g DF .text 000000000000012b Base fl_method_channel_invoke_method_finish
000000000042b410 g DF .text 000000000000007c Base fl_method_success_response_get_result
0000000000437a00 g DF .text 0000000000000050 Base fl_view_new
000000000041b890 g DF .text 000000000000007c Base fl_dart_project_get_icu_data_path
0000000000e8f8d8 g D .bss 0000000000000000 Base _end
000000000041b990 g DF .text 00000000000000a6 Base fl_dart_project_set_dart_entrypoint_arguments
00000000004364c0 g DF .text 000000000000003d Base fl_value_get_int
00000000004365f0 g DF .text 000000000000003d Base fl_value_get_int32_list
00000000004133d0 w DF .text 0000000000000005 Base operator delete(void*, std::align_val_t)
00000000004192c0 g DF .text 00000000000001c1 Base fl_basic_message_channel_new
00000000004198d0 g DF .text 000000000000015d Base fl_basic_message_channel_send
0000000000419a70 g DF .text 000000000000012b Base fl_basic_message_channel_send_finish
000000000042d980 g DF .text 0000000000000080 Base fl_plugin_registry_get_type
000000000041be60 g DF .text 0000000000000045 Base fl_engine_new_headless
0000000000436480 g DF .text 000000000000003f Base fl_value_get_bool
00000000004297b0 g DF .text 000000000000007c Base fl_method_call_get_args
000000000042ae40 g DF .text 000000000000003b Base fl_method_success_response_get_type
0000000000434f70 g DF .text 0000000000000160 Base fl_texture_registrar_mark_texture_frame_available
000000000041f790 g DF .text 0000000000000176 Base fl_json_message_codec_encode
000000000042d320 g DF .text 0000000000000154 Base fl_plugin_registrar_get_messenger
00000000004368c0 g DF .text 0000000000000073 Base fl_value_set
00000000004293f0 g DF .text 00000000000000be Base fl_message_codec_decode_message
エンジンは、合成されたシーンのラスタライズ、FlutterのコアAPIの低レベル実装の提供、ファイルとネットワークのI/Oの処理、アクセシビリティのサポート、プラグインアーキテクチャ、そしてDartのランタイムとコンパイルツールチェーンといった役割を担います。ランタイムは固定されており、アプリケーション固有のコードは一切含まれていません。
Flutterアプリは独立したライブラリですが、標準的なライブラリとしては読み込まれません。代わりに、アプリケーションのレイアウト、コード、オブジェクトを含む独自の形式に従っています。
readelf -Ws libapp.so
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000001ec000 19904 OBJECT GLOBAL DEFAULT 7 _kDartVmSnapshotInstructions
2: 00000000001f0dc0 0x2e0420 OBJECT GLOBAL DEFAULT 7 _kDartIsolateSnapshotInstructions
3: 0000000000000200 33728 OBJECT GLOBAL DEFAULT 2 _kDartVmSnapshotData
4: 00000000000085c0 0x1dfff0 OBJECT GLOBAL DEFAULT 2 _kDartIsolateSnapshotData
5: 00000000000001c8 32 OBJECT GLOBAL DEFAULT 1 _kDartSnapshotBuildId
アプリのファイルには2つのスナップショットが含まれています。一つはVMアイソレート用、もう一つは実際の中身を持つアイソレート用です。それぞれのアイソレートは、データセクション(アイソレートのヒープを含む)と命令セクション(ネイティブにコンパイルされたコードを含む)に分かれています。
FlutterはDartのアイソレートモデルを使用しています。このモデルでは、Dartのコードはすべてアイソレートの中で実行されます。アイソレートは、ヒープと呼ばれるメモリ領域で構成される構造です。Flutterでは複数のアイソレートは活用されておらず、常に存在するVMアイソレートを除けば、使われるアイソレートは一つだけです。
VMアイソレートは特別なアイソレートで、イミュータブルなオブジェクトだけを管理し、ほかのアイソレートからアクセスできます。
FlutterアプリにおけるDartスナップショットとは、実行中の特定の時点におけるDart VMのシリアライズされた状態を表すもので、ネイティブにコンパイルされたコードもすべて含みます。Flutterでは、アイソレートのスナップショットは、mainが呼び出される直前のDart VMの状態に相当します。
セットアップと実行時のパッチ適用
Flutterアプリケーションを解析するにあたり、当社が目指すのは堅牢で保守しやすいアプローチに頼ることです。ファイル形式を再実装する代わりに、Dart SDKを改変して、実行時にFlutterアプリケーションから必要な情報を抽出します。
Flutterアプリケーションをコンパイルすると、アプリケーションのディレクトリ内に2つのファイルが生成されます。

libFlutter.so:Flutterエンジンを含む共有ライブラリとして機能するファイルで、UIの描画、入力イベントの処理、アプリケーションのライフサイクル管理を担います。libapp.so:Flutterアプリケーションを構成する実際のアプリケーションコードとロジックを含むファイルです。
どちらのファイルにも、ビルドバージョンを区別するためのsnapshot_hashが含まれています。snapshot_hashはビルドバージョンの一意な識別子として機能します。libFlutter.soとlibapp.soのファイル間でsnapshot_hashが異なると、バージョン不一致エラーが発生し、アプリケーションは起動しません。
特定バージョンのFlutterにパッチを当ててビルドするには、次の手順に従います。
特定のFlutterリリース向けのパッチを作成する
Flutterリリース向けのパッチを作成するには、そのリリースで使われているDart SDKの具体的なバージョンを特定する必要があります。Flutterの公式リリースは、次のリンクで一覧できます:https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json
たとえば、Flutter v3.10.4はDart SDK v3.0.3を使用しています。

最初のステップは、その特定バージョンのDart SDK用にパッチファイルを作成することです。
git clone https://github.com/dart-lang/sdk
cd sdk
git checkout 2.13.4
次に、実行時にFlutterアプリケーションから情報をダンプするよう、Dart SDKのソースコードを編集します。これらの変更については次のセクションで詳しく説明します。すべての変更が終わったら、次のコマンドを実行して変更内容のパッチファイルを作成します。
git diff > patch_2_13_4.patch
パッチファイルは後で使うために保管しておきます。
2 - パッチを当てたDart SDKでlibFlutter.soをビルドする
Ninjaビルドシステムやgclient依存関係管理ツールなど、必要なツールを含むリポジトリをクローンし、PATHに追加します。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
cd depot_tools
export PATH=$PATH:$(pwd)/depot_tools
特定のFlutterリリースに対応する正しいバージョンのFlutter Engineを確実に使うため、リリースと同じコミットハッシュの時点のFlutterコードベースで/bin/internal/engine.versionファイルを探します。ファイルを開き、そこに記載されているコミットハッシュを確認します。このコミットハッシュが、そのFlutterリリースで使われているFlutter Engineの具体的なバージョンを表します。
例として、Flutterのバージョンv3.10.4を考えます。関連するコミットハッシュは682aa387cfe4fbd71ccd5418b2c2a075729a1c66です。
次のURL Engine.versionにアクセスすると、Flutterバージョン3.10.4で使われているエンジンが2a3401c9bbb5a9a9aec74d4f735d18a9dd3ebf2dであることが分かります。

Flutter Engineのリポジトリをクローンし、目的のFlutterバージョンで使われているものと同じコミットハッシュをチェックアウトします。
git clone https://github.com/flutter/engine.git
cd engine
git fetch origin 2a3401c9bbb5a9a9aec74d4f735d18a9dd3ebf2d
git reset --hard
gclientを使うためのディレクトリを用意し、.gclient設定ファイルを作成します。
mkdir costum_engine
cd customEngine
echo 'solutions = [{"managed": False,"name": "src/flutter","url": "PATH_TO_ENGINE'","custom_deps": {},"deps_file": "DEPS","safesync_url": "",},]' > .gclient
gclientを実行し、完了するまで待ちます。
cd customEngine
gclient sync
gclientは、Flutterエンジン内のDEPSファイルに記載されたすべての依存関係を、Dart SDKリポジトリのDart SDKも含めて、costum_engine/src/third_party/dart/フォルダーにクローンします。
Dart SDKのソースコードに何らかの変更を加えると、コンパイル時に異なるsnapshot_hashが生成され、どのlibapp.soとも組み合わせて使えない壊れたlibflutter.soになってしまう可能性があります。
snapshot_hashは、コンパイル時にmake_version.pyスクリプト内のMakeSnapshotHashString()メソッドを使って生成されます。
ソースコードを変更する前に、ソースコードを変更してもmake_version.pyが常に正しいsnapshot_hashを生成するようにしておく必要があります。
make_version.pyスクリプトを使って、元のsnapshot_hashを生成します。
cd costum_engine/src/third_party/dart/tool
ipython
In [1]: import make_version
In [2]: make_version.MakeSnapshotHashString()
Out[2]: 'e4a09dbf2bb120fe4674e0576617a0dc'
テキストエディターでmake_version.pyスクリプトを開き、元のsnapshot_hashを返すように変更します。
code make_version.py

これで、snapshot_hashの不一致を気にせずにソースコードを変更できます。パッチファイルをDart SDKのフォルダーにコピーし、適用します。
git apply patch_2_13_4.patch
これで、サポートされている任意のオペレーティングシステム/アーキテクチャ向けにFlutterをビルドできます。
costum_engine/src/flutter/tools/gn --no-goma --android --android-cpu=arm64 --runtime-mode=release
ninja -C costum_engine/src/out/android_release_arm64
完了すると、パッチを当てたlibFlutter.soが次のフォルダーに生成されます:custom_engine/src/out/android_release_arm64/lib.stripped/libflutter.so.
関数、クラス、オブジェクトの抽出
Flutterアプリケーションの構造、ライブラリの一覧、関数とその位置に関する情報をダンプするために、reFlutterはクラステーブルのオブジェクトにパッチを当ててそれらを列挙しています。新しいリリースでは関数の一覧が削られているため、このオブジェクトはもう使えません。
そのクラスでは情報が削られていますが、読み込み時のアプリケーション内には情報がまだ存在しています。これには注意点がありますが、それについては別の記事で改めて取り上げます。
通常のケースでは、Flutterのパーサーから、Flutterの関数とクラス、そしてそのオフセットを引き続き列挙できます。そのためには、FunctionDeserializationClusterクラスのPostLoad関数にパッチを当てる必要があります。
以下は、関数のオフセットをJSON形式でダンプするために適用するパッチのサンプルです。
void PostLoad(Deserializer* d, const Array& refs, bool primary) {
+ OS::Print("Patch: Function List START\n");
if (d->kind() == Snapshot::kFullAOT) {
Function& func = Function::Handle(d->zone());
for (intptr_t i = start_index_, n = stop_index_; i < n; i++) {
func ^= refs.At(i);
auto const code = func.ptr()->untag()->code();
ASSERT(code->IsCode());
+
+ auto& rCode = Code::Handle(code);
+ auto& rClass = Class::Handle(func.Owner());
+ auto& rLib = Library::Handle(rClass.library());
+ auto& rlibName = String::Handle(rLib.url());
+
+ JSONWriter js;
+ // Open empty object so output is valid/parsable JSON.
+ js.OpenObject();
+
+ js.PrintProperty("method_name", func.UserVisibleNameCString());
+ js.PrintProperty("offset", offset);
+
+ auto& sig = String::Handle(func.InternalSignature());
+ js.PrintProperty("library_url", rlibName.ToCString());
+ js.PrintProperty("class_name", rClass.UserVisibleNameCString());
+ js.CloseObject();
+
+ char* buffer = nullptr;
+ intptr_t buffer_length = 0;
+ js.Steal(&buffer, &buffer_length);
このパッチは、ライブラリのURL、クラス名、メソッド名を読み取ります。メソッドのシグネチャなど、その他の情報も抽出できますが、常に存在するとは限りません。
返されるオフセットは絶対アドレスであり、ライブラリが読み込まれたアドレスによって変わります。
バイナリ内の相対オフセットだけを表示するには、次のパッチを適用します。
code->untag()->monomorphic_entry_point_ = monomorphic_entry_point;
- code->untag()->monomorphic_unchecked_entry_point_ =
- monomorphic_entry_point + unchecked_offset;
+
+ auto& offset =
+ instructions_table_.rodata()
+ ->entries()[instructions_table_.rodata()->first_entry_with_code +
+ instructions_index_ - 1]
+ .pc_offset;
+ code->untag()->monomorphic_unchecked_entry_point_ = offset;
+ // OS::Print("Patch: Offset 0x%016lx\n", monomorphic_entry_point + unchecked_offset);
起動時にオフセットを抽出できれば、それらをインターセプトできます。オフセットはログに出力することもできますが、ログにはサイズの制限があり、データが切り詰められることが多いため、問題になる場合があります。
ファイルをディスクに書き込むほうが安定した方法です。外部ファイルに書き込むには、次のパッチを追加します。このパッチは、端末ごとに異なるファイルシステムのレイアウトに対応するため、複数のファイルの場所へのダンプを試みます。
+ for (const auto& path : PATHS) {
+ OS::Print("Using Path %s\n", path);
+ std::FILE* file;
+ // Write to the file
+ file = std::fopen(path, "a");
+ if (file != NULL) {
+ std::fwrite(buffer, sizeof(char), buffer_length, file);
+ std::fwrite("\n", sizeof(char), 1, file);
+ std::fclose(file);
+ OS::Print("Successfully wrote to the file '%s'.\n", path);
+ } else {
+ OS::Print("Failed to open the file '%s' for writing.\n", path);
+ }
+ }
+
関数とそのオフセットの一覧を収集できたら、それらのインターセプトを始められます。
動的解析とFlutterの独自ABI
Flutterで関数呼び出しをインターセプトするうえで大きな障壁となるのは、独自のABI実装を使用していることです。
ABIはバイナリ互換性を規定するもので、呼び出し規約、データ型、サイズとアラインメント、システムコールのインターフェース、名前修飾(ネームマングリング)、例外処理、ファイル形式を対象とします。
Dartは独自の呼び出し規約を使用しています。たとえばarm64の呼び出し規約は、sdk/runtime/vm/constants_arm64.hファイルで定義されています。
// Register aliases.
const Register TMP = R16; // Used as scratch register by assembler.
const Register TMP2 = R17;
const Register PP = R27; // Caches object pool pointer in generated code.
const Register DISPATCH_TABLE_REG = R21; // Dispatch table register.
const Register CODE_REG = R24;
const Register FPREG = FP; // Frame pointer register.
const Register SPREG = R15; // Stack pointer register.
const Register ARGS_DESC_REG = R4; // Arguments descriptor register.
const Register THR = R26; // Caches current thread in generated code.
const Register CALLEE_SAVED_TEMP = R19;
const Register CALLEE_SAVED_TEMP2 = R20;
const Register BARRIER_MASK = R28;
const Register NULL_REG = R22; // Caches NullObject() value.
const Register HEAP_BASE = R23;
FridaまたはLLDBのようなデバッガーで関数をインターセプトしてその引数を読み取るには、まず引数のポインターを取得する必要があります。そのうえで、引数の型に基づいて、読み取る引数オブジェクトのメモリレイアウトを判断します。引数の型は事前に把握しておくか、場合によっては関数シグネチャのメタデータから抽出できます。
以下は、引数を抽出し、文字列の値を読み取るためのFridaのJSコードです。BoolとIntは単純な値であり、同じ方法で簡単に取得できます。
...
const argPointer = dartGetArguments(this.context, index)
const stringValue = getDartStringData(argPointer)
...
/**
* Get Dart Arguments.
*
* Arguments are passed to the custom function stack pointed at by register X15 on ARM.
*
* @param context Frida context giving access to register values.
* @param argIndex Argument index to determine which offset is the arg pointer.
* @returns {*} Argument pointer.
*/
function dartGetArguments(context, argIndex) {
// RSP on x64, see constants_x64.h at Dart VM repo SPREG value.
const x15 = context.x15;
return x15.add(8 * argIndex).readPointer();
}
/**
* Read SMI (Small Integer) from pointer.
* @param smiPtr SMI Pointer,
* @returns {*|null} Value.
*/
function readSMI(smiPtr) {
let smi_data = smiPtr.readU64();
if (parseInt(smi_data & 0x1, 10) === 0) {
return smi_data >> 1;
}
console.log(
`Invalid SMI pointer ${smiPtr} -> 0x${smi_data.toString(16)}: Smi LSB should be 0`)
return null
}
/**
* Parse String Dart object to extract value.
* @param dartStringPtr Dart String pointer.
* @returns {null|*[]} Tuple of parsed data.
*/
function parseDartString(dartStringPtr) {
if (dartStringPtr.and(0x1).toInt32() === 1) {
dartStringPtr = dartStringPtr.sub(1)
}
const tag = dartStringPtr.readU32();
const classId = (tag >> 16) & 0xffff;
if (classId === 0x5 || classId === 0x55) {
let stringLength = readSMI(dartStringPtr.add(8));
let stringDataStr = dartStringPtr.add(16)
let stringData = stringDataStr.readCString(stringLength);
return [stringDataStr, stringLength, stringData]
}
return null
}
/**
* Read Dart string at pointer.
* @param dartStringPtr Dart string pointer.
* @returns {*|null} String value.
*/
function getDartStringData(dartStringPtr) {
let dartStringInfo = parseDartString(dartStringPtr);
if (dartStringInfo != null) {
return dartStringInfo[2];
}
return null;
}
ここまでで、パッチを作成し、パッチを当てたランタイムをビルドし、関数とその引数を読み取れるスクリプトを用意しました。最後のステップは、AndroidであれiOSであれ対象のアプリケーションを再パッケージし、起動して、使い慣れた計装ツールをアタッチすることです。
どの関数を計装すべきかは大きなテーマであり、今後の記事で取り上げます。その際には、120kを超えるメソッドを持つDart SDKと、人気上位1000のパッケージで見られるリスクの一部について説明します。
通信のインターセプト
Flutterの通信は一風変わっており、独自の対応が必要です。
通常のAndroidやiOSのアプリケーションでは、さまざまな方法で通信をインターセプトし、TLSピンニングを回避できます。たとえば、ネットワークセキュリティポリシーを変更して独自の証明書を受け入れるようにする、SSL_readとSSL_writeをフックして暗号化前の通信をインターセプトする、TLSのセッション鍵をダンプして通信を復号するといった方法です。
FlutterはネイティブのTLSスタックを使用しないため、プロキシを設定したり通信をインターセプトしたりすることができません。TLSライブラリはFlutterランタイムに静的にコンパイルされているため、それを特定して動的にパッチを当てることは困難です。
Flutterで通信をインターセプトするには、ここでもFlutterランタイムにパッチを当てて、TLS証明書の検証を無効にし、独自のプロキシを設定するのが最も堅牢な方法です。
このアプローチはreFlutterツールですでに使われており、現行バージョンのFlutterランタイムでも引き続き機能します。
Socket.ccでは、IPアドレスとポートを、通信のインターセプトに使うものへ強制的に変更します。
void FUNCTION_NAME(Socket_CreateConnect)(Dart_NativeArguments args) {
RawAddr addr;
SocketAddress::GetSockAddr(Dart_GetNativeArgument(args, 1), &addr);
Dart_Handle port_arg = Dart_GetNativeArgument(args, 2);
int64_t port = DartUtils::GetInt64ValueCheckRange(port_arg, 0, 65535);
+ if (port > 50) {
+ port = 8083;
+ addr.addr.sa_family = AF_INET;
+ addr.in.sin_family = AF_INET;
+ inet_aton("192.168.10.5", &addr.in.sin_addr);
+ }
+ ...
次に、ssl_crypto_x509_session_verify_cert_chainで証明書のチェックを無効にします。
static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,
SSL_HANDSHAKE *hs,
uint8_t *out_alert) {
+ return true;
これらの変更を加えた後は、端末に証明書を一切インストールせずに、プロキシを使って通信をインターセプトできます。ただし、インビジブルプロキシモードを使用していることを確認する必要があります。
まとめ
本記事の第一部では、Flutterアプリケーションのレイアウト、ランタイムにパッチを当てる方法、関数呼び出しをインターセプトしてその引数を読み取る方法、そして最後に、通信をインターセプトするためにランタイムにパッチを当てる方法を説明しました。
続く2本の記事では、Dartアプリケーションで評価すべきセキュリティ上の問題を取り上げます。主な問題としては、dart:ffiを使用した際のメモリ破壊の脆弱性、dart:jniやdart:jsを使用した際の相互運用性の問題、リフレクション、そしてネイティブコードまたはReflectablesを使用したDartコードでのシリアライゼーションの問題があります。
また、アプリケーションを開発する際に考慮すべき、人気のDartパッケージにおける既知のセキュリティ上の問題についても説明します。