レビューでは防げないインジェクション
コードインジェクションは、データがコードになる継ぎ目で止められます。個々のケースを判断するのではなく、構文上のクラス全体を拒否することで止めます。これはその境界が決定可能な場所ならどこでも成り立ちます。自然言語のプロンプトインジェクションが決して持たない性質です。
AI エージェントを安全に保つことについて、安心できる筋書きがあります。人間をループに入れる、というものです。エージェントが提案し、人間が承認し、悪意のあるものはすべて門の前で捕まえる。私はまさにそういう形のパイプラインを作っています。エージェントが Web を走査し、下書きをまとめ、何かが公開される前にそれを人に渡します。そして、何が実際にそれを安全に保っているのかを長く見るほど、その安全性のうち人間の承認が占める割合は少ないとわかってきます。コードの実行を止める部分は、下書きがレビュー担当者の目に届く前に、自ら決着をつけます。そのどれもが何かを判断してはいません。
インジェクションが実際に潜む場所
重要なインジェクションは、人間がレビューする指示の中にはありません。データの中にあります。ソースの一部はユーザー生成、つまり Reddit のスレッドや Hacker News のコメントで、定義からして信頼できません。そしてその下書きが書かれている形式である MDX は、波かっこをコードとして扱います。{something} のような式は JavaScript の式ノードです。信頼できないテキストに、かっこをコードとして読む形式が加わると、コンテンツの中にデータからコードへの継ぎ目ができます。
パイプラインにはその継ぎ目に餌を与えないルールがあります。生のフォーラムのテキストが下書きにそのまま入ることはなく、エージェントはそこから話題だけを取り、本物の引用はすべて検証済みのソースに遡れて、コードフェンスの中に置かれなければなりません。しかしルールはエージェントへの指示であり、指示こそこのエッセイが信頼しないと述べている種類の保証です。それでも波かっこが MDX に届いてしまったら、ルールを無視したエージェント、フェンスの付け方を間違えた引用、そうした場合に、安全性はルールが守られることに依存していてはならなかったのです。だからこそ、実際にそれを止める部分はもっと下にあり、かっこがどうやってそこに来たかを気にしません。
これは lint を正当化するために私がでっち上げた危険ではありません。2026 年 7 月、Hugging Face はまさにこの形の侵入について、一次情報としての時系列を公開しました。データセットの設定、つまりデータが、Jinja2 のテンプレートインジェクションと HDF5 のファイル読み込みを運び、人間がループのどこにもいない自律エージェントによって処理されたのです。ファイルがペイロードであり、それをレンダリングすることが実行でした。データがコードになり、迂回すべきレビュアーはいませんでした。レビュアーはそもそも経路上にいなかったからです。
継ぎ目は決定可能である
MDX のケースで重要なのは、その境界が構文的だということです。MDX には文法があります。パーサーは木をたどり、どの式ノードも確信を持って指し示せます。これはコード、それは散文、と推測なしに。区別が意味から推し量られるのではなく、言語そのものに書き込まれているからです。下流にあるあらゆる本物の防御はそこに立っています。
レンダラーはすでにそれを活用できます。next-mdx-remote という MDX レンダラーには、既定で有効な blockJS オプションがあります。このオプションは、どの式が危険に見えるかを判断するスキャナーではありません。パースされた木をたどり、何かがレンダリングされる前に、すべての式ノードをまるごと削除します。悪意のある {fetch(evilUrl)} も無害な {1 + 1} も同じ末路を迎えます。削除です。判断されるのでもサンドボックスに入れられるのでもなく、拒否されます。構文的な継ぎ目では判断する必要がないからです。クラス全体を禁止できるのです。
二つのチェック、どちらも判断しない
パイプラインはそのチェックの前に、さらに大雑把な二つ目のチェックを置きます。プレビューサーバーが起動する前に、lint が frontmatter を取り除き、フェンスで囲まれたコードブロックを取り除き、インラインコードを取り除き、それ以外のどこかに波かっこが一つでも残った瞬間に失敗します。パースもしなければ、意図について推論もしません。フェンスの外の波かっこが、開きでも閉じでも一つあれば、下書きはそれ以上進みません。
フレームワークがすでに式を取り除くのに、なぜ手間をかけるのか。二つが別々の失敗を捕まえるからです。整った式は blockJS が削除します。そこでは lint は安全性のためには冗長ですが、文章のためには冗長ではありません。それがなければ、引用から持ち込まれた波かっこは、公開された記事から静かに消え、欠けた一文に誰も気づきません。壊れた波かっこ、相方のない単独の { は、削除の段階にそもそも到達しません。まず MDX としてパースに失敗するからで、それはプレビューのルートが要求されたときに、何層も奥のコンパイルエラーとして現れます。lint は両方を、dev サーバーが動き出す前に、名前のついた失敗に変えます。
私はこれを過大に売り込みません。二つは同じ種類のものではないからです。blockJS はレンダリング経路の内側で動き、それなしには何もレンダリングされません。lint はパイプラインが最初に実行するよう言われているステップで、ステップは飛ばせます。コードの何もそれを強制しません。ですからそれは二枚目の壁というより、より早く、より安いもう一枚の壁であり、加えて、誰かが blockJS を切ってレンダリング経路が自分を守らなくなる日への備えです。それはそれ自身の基準でも大雑把です。パーサーではなく正規表現で、本物の MDX パーサーにはないバックティックの不一致という穴があり、そして短いインジェクションフレーズのブロックリストは、まさにこのエッセイの残りが信頼するなと言う、言語側の穴だらけのフィルターそのものです。ほとんどただ同然だから残してあるだけです。ここから覚えておく価値のあるルールが一つ残ります。継ぎ目を仲介するのは、それを実際にレンダリングするパーサーで行い、そっくりさんで代用しないこと。二つの間の隙間こそ、密輸されたペイロードが棲む場所だからです。
自然言語の継ぎ目にはそのようなゲートがない
Simon Willison は同じ問題に言語の側から何年も取り組んできました。その結論は私のものの鏡像です。プロンプトインジェクションに信頼できる検知器は存在しない、というものです。誰も十分に賢くないからではなく、その境界が構文的でないからです。悪意のある指示と正当な指示は、同じ素材、つまり言葉でできていて、どんなパーサーもその木をたどって「このトークンはデータ、あれはコード」と裁定できません。フィルターはかけられ、いくらかは捕まえられ、そして敵はフィルターを避けて書き換えます。彼の「lethal trifecta」、すなわち信頼できないコンテンツ、非公開データへのアクセス、そのデータを外へ送り出す手段は、その継ぎ目が高くつく条件を名指しします。
それが分かれ目です。コードとデータの境界が構文的なら、決定可能な継ぎ目が得られ、検知器もレビュアーもなしにクラスをきっぱり禁止できます。それが意味的なら、モデル自身のコンテキストの内側では、それはできず、ループの中の人間もその隙間を埋めません。人間は同じ失敗を、より遅く、より自信のある検問所へ移すだけです。
その一手と、それが止まる場所
ですから原則は「もっと厳しくレビューせよ」ではなく、「ビルドを失敗させよ」ですらありません。それはこうです。構築の段階でコードをデータから分離せよ、一方が他方になる継ぎ目で、何かが評価される前に。やり方は二つあります。文法が両側にラベルを付けてくれるか、MDX は式に印を付け、あなたは印の付いたクラスを削除します。あるいはインターフェースが両者を別々のチャネルで運ぶか、パラメータ化されたクエリが文を一方の経路で、値をもう一方の経路で送るように、下流の何もどちらがどちらかを決めずに済むようにします。クラスを禁止するのは安上がりな特殊ケースで、危険な形が信頼できないチャネルで正当な用途を持たないときにだけ使えます。波かっこは当てはまります。散文で波かっこが要ることは決してないからです。文字列連結の SQL は当てはまりません。だからそこでの解決は禁止ではなく、別々のチャネルなのです。安全なデシリアライザとテンプレートのサンドボックスは、同じ継ぎ目にもっと手前から手を伸ばし、型の許可リストや囲われたレンダリングを使います。それらはコストが高く、抜けられもします。文法がラベルを付けないところで裁定するからです。いちばんすっきりしたゲートは知能をまったく必要としません。残りは判断で安全を買い、抜け穴でその代金を払います。
そして継ぎ目が決定可能でないとき、つまり「コード」がモデルのコンテキストに棲む指示であるとき、両側にラベルは付けられず、チャネルも分けられません。テキストから指示を読み取ることが、製品そのものだからです。とはいえ道具箱が消えるわけではありません。移るのです。あなたは指示にゲートをかけようとするのをやめ、代わりに能力にゲートをかけます。エージェントが何を呼び出せるか、何に到達できるか、何を外へ送り出せるか。trifecta から脚を一本切り落とせば、決定不能な継ぎ目は問題でなくなります。あなたはインジェクションを検知したのではありません。それが手を伸ばしていた対象を取り上げたのです。
それが私をあのレビュー・ゲートへ連れ戻します。コード実行のクラスは、誰かが下書きを開く前に決着していました。交渉しない二つのチェックが、答えが決して意見の問題でない継ぎ目で、すでにすべての波かっこを拒否していたのです。しかしそのクラスは危険のすべてではありません。悪意のあるリンクは問題なくレンダリングされ、生のマークアップは手つかずで通り、偽の主張は真の主張と同じくらいきれいに読めます。そのどれも波かっこではないので、どのゲートも捕まえません。そしてそのすべてこそ、人間の読みが担うものです。機械は決定可能なものを引き受け、人が決定可能でないものを引き受けます。それは、レビュー担当者を最も重要でない一線にするのではなく、決定可能な部分が片づいたあとに残る唯一の一線にするのです。