そういった開発者の負担を少しでも軽くすることができるように、本連載ではJavaにおけるWebアプリケーション開発時に最もよく利用されているStrutsフレームワークの実装に踏み込んで、セキュリティ上注意すべきポイントを解説していきたい。なお、本連載ではStruts1.2.8を対象として解説を行っていくが、すでにStrutsを利用したWebアプリケーション開発を行っている開発者をターゲットとしているため、Strutsの使用方法、各機能の詳細な説明などは割愛する。
| 【参考連載記事】 Webシステムのセキュリティ要件(情報マネジメント) Strutsを使うWebアプリケーション構築術(Java Solutionフォーラム) |
今回は、クロスサイトスクリプティング(Cross Site Scripting:XSS)の脆弱性に関する基本的な実装上の注意点を解説していこう。詳細については「クロスサイトスクリプティング対策の基本」における説明を参照していただくとして、簡潔に述べるとクロスサイトスクリプティングとはユーザーの入力値を基に動的なHTTPレスポンスを生成するアプリケーションにおいて、入力値として渡されたスクリプトコードがWebブラウザ上で動作することに起因する脆弱性のことである。
この脆弱性から発生する被害は、
- スクリプトがサイトをまたがることでCookieが盗まれてしまう
- VBScriptによりローカルのファイルを操作されてしまう
- HTMLタグが入力値に渡されることでページが改ざんされてしまう
などさまざまである。対策の基本となるのは、HTMLエンコード、URLエンコードと呼ばれるエスケープ処理を適切に行うことである。
bean:writeタグを使ってエスケープ処理を適切に行う
クロスサイトスクリプティングの問題は、HTML特殊文字をエスケープさせて対策することが基本である。エスケープが必要なHTML上の特殊文字とは、
「<」「>」「&」「"」「'」
などである。動的なパラメータをHTMLに出力する場合には、これらを特殊な意味を持たない文字列に変換するHTMLエンコード処理が必須となる。
Strutsを使用して動的なパラメータを扱う場合には、ViewコンポーネントであるStrutsカスタムタグライブラリを使用することが多いだろう。Strutsカスタムタグライブラリを使用する際にHTMLエンコード処理を適切に行うには、どのような注意が必要となるだろうか。代表的なカスタムタグであるbean:writeタグを使用した際の動作を見ながら確認していこう。
まず、入力されたコメントを表示するだけの単純なWebアプリケーションを作成してみる。
| <%@page contentType="text/html; charset=Shift-JIS" pageEncoding="Shift_JIS" %> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <html:html> <head> </head> <body> <html:form action="BeanWrite.do"> コメントを入力してください。 <html:text property="comment" /><br> <br> 入力されたコメント: <bean:write name="TestForm" property="comment" /><br> <html:submit value="送信"/> </html:form> </body> </html:html> |
| beanWrite.jsp |
作成したWebアプリケーションに対し、スクリプトとして動作する値を入力して送信ボタンを押したところ、以下のような画面が表示された。
![]() |
| 画面1 スクリプトとして動作する値を入力 |
スクリプトは動作せず、「<script>alert('XSS')</script>」という値がそのまま出力されている。beanWrite.jsp上でHTMLエンコードに関する特別な記載はしていないが、画面1のHTMLソースは以下のようになっており、内部的に何らかの処理が行われていることは間違いないだろう。
| <html> <head> </head> <body> <form name="TestForm" method="post" action="/WebAppSec/BeanWrite.do"> コメントを入力してください。 <input type="text" name="comment" size="50"value="<script>alert('XSS')</script>"><br> <br> 入力されたコメント: <script>alert('XSS')</script> <br> <input type="submit" value="送信"> </form> </body> </html> |
| 画面1のHTMLソース |
Struts内部の挙動を探る
Strutsの内部において、どのような変換処理が行われているのかを探るため、Strutsのカスタムタグに関するソースを見ていくことにする。Strutsカスタムタグの実体はtldファイルにおいて定義されており、使用されるクラスはtagclass要素に記載されている。
bean:writeタグの情報が記載されているstruts-bean.tldを参照すると、tagclass要素としてorg.apache.struts.taglib.bean.WriteTagクラスが定義されていることが分かる。Strutsのソースファイルを入手してorg.apache.struts.taglib.bean.WriteTagクラスの実装を確認してみるとdoStartTag()メソッド内において以下の処理が行われていた。
| 前略 // Look up the requested property value Object value = TagUtils.getInstance().lookup(pageContext, name, property, scope); if (value == null) { -(1) return (SKIP_BODY); // Nothing to output } // Convert value to the String with some formatting String output = formatValue(value); // Print this property value to our output writer, suitably filtered if (filter) { -(2) TagUtils.getInstance().write(pageContext, TagUtils.getInstance().filter(output)); } else { TagUtils.getInstance().write(pageContext, output); } 後略 |
doStartTag()メソッド内の処理 |
(1)の行で条件分岐に使用されているvalueという変数には、property属性で定義された値が入るようだ。今回の例ではリクエストパラメータのcommentがそれに当たる。値がnullでない場合には、formatValue()メソッド(数値型のフォーマット処理などを行うメソッド)から受け取った戻り値を基に後続の処理を行うように実装されている。この時点でのvalue変数の中身は「<script>alert('XSS')</script>」という値になっているはずだ。
次に、filterという変数に対して条件分岐処理を行っている(2)の行に注目してほしい。filter変数はbean:writeタグに記述できるfilter属性と関連していて、以下のように動作することがマニュアルに記載されている。
- trueが設定されていれば、HTMLエンコード処理を行う(デフォルト値)
- falseが設定されていれば、HTMLエンコード処理を行わない
filter属性がtrueの場合、もしくは今回のケースのようにfilter属性が記載されていない場合は、valueの値がTagUtils.getInstance().filter()メソッドに渡される。TagUtils.getInstance().filter()メソッドの内部ではさらにResponseUtils.filter()メソッドへvalueの値を引き継ぐ仕組みとなっている。
このメソッドの中身を確認すると、
| 前略 switch (value.charAt(i)) { case '<': filtered = "<"; break; case '>': filtered = ">"; break; case '&': filtered = "&"; break; case '"': filtered = """; break; case '\'': filtered = "'"; break; } 後略 |
ResponseUtils.filter()メソッド |
という実装があり、このメソッドにおけるHTMLエンコード処理により、valueの値が、
| <script>alert('XSS')</script> |
へと変換されることが確認できる。
bean:writeタグはこのような流れでHTMLエンコード処理を行う実装となっていた。以上の確認で、「bean:writeタグにおけるproperty属性の値はfilter属性にtrueが設定されていた場合、安全な形で出力される」ということが分かる。
ただし、各タグにおいて個別に実装されるdoStartTag()メソッド内にHTMLエンコード処理が記述されているため、ほかのタグを使用する場合も同様に、どのようなエスケープ処理が行われているかを確認する必要があるだろう。
主なタグにおけるエスケープ処理の実装状況
次の一覧はHTML出力を行う主なタグに関して、bean:writeタグと同様の方法でエスケープ処理の実装状況を調査した結果である。
BeanTag
| タグ名 | 動的パラメータの 出力 | エスケープ処理 |
|---|---|---|
| message | 可 | エスケープされない |
| write | 可 | filter属性がfalseに設定されていない場合、HTMLエンコードされる |
HTMLTag
| タグ名 | 動的パラメータの 出力 | エスケープ処理 |
|---|---|---|
| base | 不可 | エスケープされない |
| button | 不可 | エスケープされない |
| cancel | 不可 | エスケープされない |
| checkbox | 不可 | エスケープされない |
| errors | 可 | エスケープされない |
| file | 不可 | エスケープされない |
| form | 不可 | エスケープされない |
| frame | 可 | 参照先がURLエンコードされる |
| hidden | 可 | valueがHTMLエンコードされる |
| html | 不可 | エスケープされない |
| image | 不可 | エスケープされない |
| img | 可 | 参照先がURLエンコードされる |
| javascript | 不可 | エスケープされない |
| link | 可 | 参照先がURLエンコードされる |
| multibox | 不可 | エスケープされない |
| option | 不可 | エスケープされない |
| options | 可 | filter属性がfalseでない場合、表示値、valueがそれぞれHTMLエンコードされる |
| optionscollection | 可 | filter属性がfalseでない場合、表示値、valueがそれぞれHTMLエンコードされる |
| password | 可 | valueがHTMLエンコードされる |
| radio | 不可 | エスケープされない |
| reset | 不可 | エスケープされない |
| rewrite | 可 | 参照先がURLエンコードされる |
| select | 不可 | エスケープされない |
| submit | 不可 | エスケープされない |
| text | 可 | valueがHTMLエンコードされる |
| textarea | 可 | textareaタグで囲われた値がHTMLエンコードされる |
| xhtml | 不可 | エスケープされない |
NestedTag
nested:writeタグならばbean:writeタグと同様の結果となる。プロパティを取得するロジックのみが拡張されており、エスケープ処理は継承元の各タグのロジックが使用されていた。
エスケープ処理が行われないカスタムタグ
調査結果から動的パラメータを扱うタグにおいても、エスケープ処理を行わないケースがあることが分かった。bean:message、html:errorsが該当するタグである。これらのタグは、HTMLに出力する動的パラメータとして4つまで引数を受け取ることが可能だが、パラメータを扱う際に、HTMLエンコード処理をするように実装されてはいなかった。
html:errorsタグを用いたWebアプリケーションを作成して動作を確認してみよう。
| 前略 ActionMessages errors = new ActionMessages(); //リクエストパラメータuserIdをActionMessageの引数に与える errors.add("addUser.errors", new ActionMessage("addUser.error1", userId)); saveErrors(request, errors); 後略 |
| AddUserAction.do |
| 前略 <html:errors /> 後略 |
| AddUser.jsp |
| 前略 addUser.error1=ユーザID {0} は既に使用されています。 後略 |
| ApplicationResources.properties |
![]() |
| 画面2 html:errorsタグを使ったWebアプリケーションではクロスサイトスクリプティングが起きる |
リクエストパラメータであるuserIdをActionMessageの引数として与えたところスクリプトが動作した。動的なパラメータを引数として受け取らないケースではこのような問題は発生しないが、使用時には細心の注意を払う必要があるだろう。
エスケープ対象とならない属性
エスケープ処理が実装されているタグであっても、すべての属性に対して処理が行われるわけではないことが分かった。動的なパラメータの指定方法が定義されていない属性に関してはエスケープ処理の対象外であると考えてよいだろう。
| <%@page contentType="text/html; charset=Shift-JIS" pageEncoding="Shift_JIS" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <html> <body> <html:img src="dummy" alt='<%= request.getParameter("alt") %>' /> </body> </html> |
htmlImg.jsp |
![]() |
| 画面3 alt属性の値はHTMLエンコード処理の対象とならない |
画面3においてalt属性の値はHTMLエンコード処理の対象とはならないため、危険な動作をすることになる。
カスタムタグを使った場合の安全な実装方法
このようにStrutsカスタムタグを使用する際にも方法によっては危険なケースがある。それぞれの問題に対する解決手段の例を記載しよう。
●危険なカスタムタグを使用しない
html:errorsタグのように使用方法によって危険となり得るタグはできるだけ使用しない方がよいだろう。この問題は代替手段としてhtml:messagesタグを使用することで回避できる。html:messagesタグはhtml:errorsタグよりも柔軟な構成でメッセージを表示できることから推奨されているタグであり、以下のように使用する。なおbean:messageタグについても同様の対応が可能である。
| 前略 <html:messages id="msg"> <bean:write name="msg"/> </html:messages> 後略 |
html:messagesタグ |
この構成で表示すればHTMLの出力はbean:writeタグを通して行われる。bean:writeタグはfilter属性にfalseを設定しない限り、HTMLエンコード処理を行う実装となっているから、安全性が保証されたプログラムを作成することが可能だ。
●エスケープ対象とならない属性を安全に表示する
エスケープ対象とならない属性を動的に扱いたい場合は、以下のようにHTMLエンコード処理を通過させた変数を指定すればよい。
| 前略 <% String alt = org.apache.struts.util.ResponseUtils.filter(request.getParameter("alt")); %> <html:img src="dummy" alt='<%= alt %>' /> 後略 |
スクリプトレットの使用 |
スクリプトレットの使用が望ましくない場合は、独自のカスタムタグを作成することで対応してほしい。
以上、Strutsを使用したWebアプリケーション開発時における基本的なクロスサイトスクリプティング対策時の注意点を解説した。次回は、引き続きクロスサイトスクリプティングの脆弱性に関する更なる注意点について解説していこう。
http://www.atmarkit.co.jp/fsecurity/rensai/struts01/struts01.html




