Flexible GraphRAG: Cypher/グラフインジェクション。FalkorDBアダプタの_escape()関数における不適切な文字列エスケープにより、取り込まれたドキュメントの内容全体がグラフから削除される脆弱性。
**説明**
def _escape(v: str) -> str:
return str(v).replace("'", "\\'").replace("\\", "\\\\")
エスケープの順序が逆になっており、これは古典的なHTMLのダブルエスケープバグと全く同じです。ステップ1では、すべての'の前にリテラルバックスラッシュを挿入し、ステップ2では、挿入されたばかりのバックスラッシュを再度マッチさせ、それを2倍にします。シングルクォートで終わる入力の場合、2つのステップを組み合わせると、出力には\\"(2つのバックスラッシュの後にクォートが続く)が生成されます。これは、Cypher/openCypherのシングルクォートで囲まれた文字列リテラルで使用される標準的なバックスラッシュエスケープ規則に従うと、1つのエスケープされたバックスラッシュと、エスケープされていない文字列終端のクォートとして解釈されます。クォートは無効化されず、文字列が途切れます。
このエスケープされた値は、同じメソッド内の5つの箇所で、文字列フォーマット(パラメータではない)を使用して構築されたCypherクエリ文字列に直接挿入されます。
self.lc_graph.query(
f"MERGE (n:`{node.type}` {{id: '{_escape(node.id)}'}}) "
f"{props}"
f"RETURN distinct 'done' AS result"
)
...
self.lc_graph.query(
f"MATCH (a:`{rel.source.type}` {{id: '{_escape(rel.source.id)}'}}), "
f"(b:`{rel.target.type}` {{id: '{_escape(rel.target.id)}'}}) "
f"MERGE (a)-[r:`{rel_type}`]->(b) "
f"{props}"
f"RETURN distinct 'done' AS result"
)
node.type、rel.source.type、rel.target.type、およびrel_typeも、エスケープ処理なしでバッククォートで囲まれた識別子に挿入されます。
攻撃者が制御するデータがどこから来るのか:ノード/リレーションオブジェクトは、取り込まれたドキュメントの内容から抽出されたエンティティと関係であり、これがGraphRAGパイプラインの目的です。したがって、ドキュメントの内容は、このデータベース層に対しては信頼できない入力となります。これは、典型的な間接的なプロンプトインジェクションからクエリインジェクションへの連鎖です。ドキュメントを作成し、そのテキストが抽出LLMに、ペイロードを含むエンティティIDを生成させ、それを取り込みます。すると、脆弱なエスケープ関数がそれを実行可能なCypherコードに変換してしまいます。
**影響**
`lc_graph.query()`への完全なCypher句のインジェクションが発生します。この関数は、クエリパラメータ化や読み取り専用制限が一切適用されない状態で実行されます。実証された影響として、2つ目の句(`MATCH (m) DETACH DELETE m`)をクエリに追加すると、グラフ内のすべてのノードとリレーションが、1回の呼び出しで削除されます。同様の仕組みは、データの漏洩(例えば、他のノードのプロパティを、呼び出し元が読み取るフィールドを通じて返す)や、既存のレコードの改ざんにも利用できます。これは、呼び出し元のコードがエンドユーザー/LLMにどのような情報を公開するかによって異なります。
概念実証(実行済み、完全なクエリの再構成)
実行されたのは、2行の`_escape()`関数と、f-stringテンプレートのみです。`flexible-graphrag`パッケージ自体はインポートされていません。
攻撃者が制御する生のノードID:
"pwn' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //"
脆弱な`_escape()`関数の出力:
"pwn\\' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //"
実際に`lc_graph.query()`に送信された完全なクエリ文字列:
MERGE (n:`Entity` {id: 'pwn\\' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //'}) RETURN distinct 'done' AS result
解析トレース(シングルクォートで囲まれたCypher文字列の標準的なバックスラッシュエスケープ規則):
'Opening ' は、ID文字列リテラルの開始を示します。
"pwn" は、そのまま文字として解釈されます。
\\ (2つのバックスラッシュ文字) は、1つのエスケープされたバックスラッシュシーケンスとして解釈され、文字列には1つのリテラルバックスラッシュが含まれるようになります。
次の文字がエスケープされていない ' の場合、文字列リテラルが早期に終了します。結果として、IDの値は "pwn\"" (末尾にバックスラッシュがある) となります。
残りのテキスト }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn は、文字列の内容ではなく、有効なCypher構文として解釈されます。
}) は、プロパティマップとノードパターンを閉じ、有効なMERGE句を完了させます。
MATCH (m) DETACH DELETE m RETURN count(m) AS pwn は、MERGE句の直後に続く、2番目の完全な有効なCypher句です (Cypherでは、区切り文字なしで連続した句を使用できます)。
// は、行コメントの開始を示し、残りの元のテンプレート ('}) RETURN distinct 'done' AS result) がすべて消費されるため、実行されません。
実際にデータベースに送信されるクエリは以下のようになります。
MERGE (n:`Entity` {id: 'pwn\'}) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn
- これは、1つの不要なノードを作成し、その後、グラフ全体を削除する、構文的に完全な2つのCypher句からなるステートメントです。
提案される修正:
' の前にバックスラッシュを付加する(この修正は、litellm の報告と同様のカテゴリに属し、並べ替えのバグを修正します)。
def _escape(v: str) -> str:
return str(v).replace("\\", "\\\\").replace("'", "\\'")
より強力な修正として、以下の方法を推奨します。文字列の補完を完全に避け、FalkorDB のパラメータ化されたクエリのサポート (lc_graph.query(query, params={...})) を使用し、node.type/rel_type/rel.source.type/rel.target.type に含まれるバッククォート文字を、それらをラベルまたは関係タイプの識別子として使用する前に、拒否またはエスケープします(現在、これらの文字に対するエスケープ処理は一切行われていません)。
注意点(元の内部での調査と同様):
これは、純粋な文字列構築のバグであることが確認されました(上記の手順 1~8 は仮定ではなく、実際に実行されます)。また、FalkorDB の Cypher 互換ドキュメントに記載されている文字列リテラルのためのバックスラッシュのエスケープ規則と一致していることが確認されましたが、パーサーの正確な動作をバイト単位で確認するために、実際の FalkorDB インスタンスでテストは行われていません。FalkorDB の実装がこの規則から逸脱している場合、上記の具体的な PoC (Proof of Concept) ペイロードをわずかに修正する必要があるかもしれません。いずれにしても、根本的なエスケープ順序の欠陥自体は確認されています。
メンテナの注記:実際の FalkorDB に対して、この脆弱性が悪用可能であることが確認されました。FalkorDB では、MERGE と MATCH の間に WITH が必要です。そのため、ペイロードに "WITH 1 AS x" を追加する必要があります。これによって、ペイロードはグラフ内のすべてのノードを削除しました。0.8.2 で修正されました。値はクエリパラメータとして渡され、ラベル/関係タイプ/プロパティキーからバッククォートが削除されます。
影響を受ける設定:GRAPH_BACKEND=langchain で、GRAPH_DB=falkordb の場合のみ。LlamaIndex の FalkorDB ストアは影響を受けません。
アップグレード前の回避策:GRAPH_BACKEND=llamaindex で FalkorDB を使用するか、別のグラフデータベースを使用してください。
影響を受けるパッケージ:
- pip (Python) flexible-graphrag: 影響を受けるバージョンは 0.6.0 以上、0.8.2 未満。0.8.2 で修正されました。