Strutsでは、日付フィールドなどの範囲チェックや、必須チェック、形式チェックなどさまざまな入力値チェックを容易に実装するために、Validatorプラグインという仕組みが提供されている。
| 【参考記事】 Validatorによる妥当性検証の実現(前編)(Java Solution) |
今回は、Struts 1.2.8におけるValidatorプラグインの仕組みをセキュリティ側から考察し、使用上の注意点を探っていく。
Validatorプラグインの仕組み
まず、Validatorプラグインを使用したサンプルアプリケーションを作成して、動作の仕組みを見ていこう。
| 前略 <action path="/RegistUser" type="vlunApp.RegistUserAction" validate="true" -(1) name="UserForm" -(2) scope="request" input="/pages/registUser.jsp"> <forward name="success" path="/pages/registUserComp.jsp" /> </action> 後略 |
| struts-config.xml |
| 前略 <form name="UserForm"> -(3) <field property="userId" depends="required,mask"> <arg key="UserForm.userId"/> <var> <var-name>mask</var-name><var-value>^[0-9a-zA-Z]*$</var-value> </var> </field> <field property="password" depends="required,mask"> <arg key="UserForm.password"/> <var> <var-name>mask</var-name> <var-value>^[0-9a-zA-Z]*$</var-value> </var> </field> </form> 後略 |
| validation.xml |
| public class RegistUserAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserForm userForm = (UserForm) form; String userId = userForm.getUserId(); String password = userForm.getPassword(); String sql = "insert into m_user (user_id,password) values ('" + userId + "','" + password + "')"; DBMgr db = new DBMgr(); Connection conn = db.getConnection(); Statement stmt = conn.createStatement(); try { stmt.executeUpdate(sql); conn.commit(); } finally { 中略 } return mapping.findForward("success"); } } |
| RegistUserAction.java |
| public class UserForm extends ValidatorForm { private String userId; private String password; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } } |
| UserForm.java |
作成したサンプルアプリケーション上で、UserIDに「'(シングルクオーテーション)」を入力して、OKボタンを押した際の挙動は以下となる。UserIDが形式チェックされていることが分かるだろう。
![]() |
| 画面1UserIDに「'(シングルクオーテーション)」を入力して、OKボタンを押す |
このサンプルアプリケーションにおいて、Validatorに関連している各設定の意味を以下に記述する。
(1)action要素のvalidator属性にtrueを設定することで、検証機能(入力値のチェック機能)が有効となる
(2)ActionパスとValidatorFormが関連付けられる
検証の定義を持つValidatorFormを継承して作成したFormクラスと、Actionパスが関連付けられる。
(3)ValidatorFormと検証ルールが関連付けられる
ここでは、必須チェックと形式チェックを行っている。
上記の定義を基に検証処理を制御しているのが、RequestProcesser#processValidateメソッドである。
| protected boolean processValidate(HttpServletRequest request, HttpServletResponse response, ActionForm form, ActionMapping mapping) throws IOException, ServletException { if (form == null) { -(4) return (true); } // Was this request cancelled? if (request.getAttribute(Globals.CANCEL_KEY) != null) { -(5) return (true); } // Has validation been turned off for this mapping? if (!mapping.getValidate()) { -(6) return (true); } // Call the form bean's validation method ActionMessages errors = form.validate(mapping, request); if ((errors == null) || errors.isEmpty()) { -(7) return (true); } 中略 return (false); } |
| RequestProcesser.java(ロギング部分を省略した抜粋) |
RequestProcesser#processValidateメソッドは、検証処理の結果が妥当であればtrueを返し、妥当でない場合falseを返す動作をする。
(4)struts-config.xmlにおいて、action要素のname属性が設定されていない場合は、検証を行わずにtrueを返す
(5)Globals.CANCEL_KEYという属性がリクエストスコープに存在した場合は、検証を行わずにtrueを返す
(6)struts-config.xmlにおいて、action要素のvalidate属性がtrueでない場合は、検証を行わずにtrueを返す
(7)検証を行った結果が妥当であった場合は、trueを返す
(4)、(6)、(7)については、各設定ファイルの定義を反映した挙動である。ただし、(5)の動作は設定ファイルに定義されているものではない。Globals.CANCEL_KEYという属性が存在した場合、無条件にtrueを返すように読み取れる挙動はどのような意図で実装されているのだろうか。
StrutsのCancel機能
Strutsには、複数のボタンが配置されたformにおいて、片方のボタンが押された場合は入力値チェックを行うが、もう片方のボタンが押された場合は入力値チェックを行わないといった制御を実装するために、Cancel機能というものが存在する。「戻る」ボタンを実装する場合などに使用されることがある機能であり、html:cancelタグと併用して動作する。
html:cancelタグを使用した場合、
| <input type="submit" name="org.apache.struts.taglib.html.CANCEL" value="Cancel"> |
のようなsubmitボタンがHTMLとして作成される。Cancelボタンが押されたかどうかをAction#isCancelledメソッドを呼び出した戻り値によって判断し、後続の処理を分岐させることが可能な仕組みである。
| protected void processPopulate(HttpServletRequest request, HttpServletResponse response, ActionForm form, ActionMapping mapping) throws ServletException { 中略 if ((request.getParameter(Constants.CANCEL_PROPERTY) != null) || (request.getParameter(Constants.CANCEL_PROPERTY_X) != null)) { request.setAttribute(Globals.CANCEL_KEY, Boolean.TRUE); } } |
| RequestProcesser.java |
(5)において、使用されるGlobals.CANCEL_KEYという属性は、Constants.CANCEL_PROPERTYまたはConstants.CANCEL_PROPERTY_Xがリクエストパラメータに含まれていた場合に、セットされるようRequestProcesser内で実装されている。
Constants.Constants.CANCEL_PROPERTYはorg.apache.struts.taglib.html.CANCELと、Constants.Constants.CANCEL_PROPERTY_Xはorg.apache.struts.taglib.html.CANCEL_Xと、それぞれ一致する。
従って、「Globals.CANCEL_KEYという属性が存在した場合、検証を行わずにtrueを返す」という挙動はCancel機能を実現させるために実装されていることが分かる。
Cancel機能による入力値チェックの回避
Cancel機能により、html:cancelタグを使用して生成されたsubmitボタンが押された場合、入力値チェックはスキップされる。それでは、Cancel機能の使用を想定していないアプリケーションに対して、org.apache.struts.taglib.html.CANCELをパラメータに与え、リクエストを送信した際には、どのような挙動が示されるだろうか。
サンプルアプリケーション上で、先ほどと同様UserIDに「'」を入力し、org.apache.struts.taglib.html.CANCELをリクエストパラメータに付加して、OKボタンを押下した際の挙動が以下である。
![]() |
| 画面2 org.apache.struts.taglib.html.CANCELをリクエストパラメータに付加して、OKボタンを押す(画像をクリックすると拡大します) |
入力値チェックがかからない状態で、Actionクラスへ処理が渡ってしまう結果となった。Action#isCancelledメソッドによるCancel状態のチェックを行っていないアプリケーションでは、入力チェックを通過していない値が後続の処理に渡されてしまう。サンプルアプリケーションでは、バインド変数などの安全な仕組み【注】を使用せずに、SQLを組み立てているため、入力値チェックを回避されてしまうと、SQLインジェクションの危険が発生することになる。
| 【注】 PreparedStatementを使用していれば、入力チェックを回避されても、SQLインジェクションの危険性を軽減できる。多層の防御態勢を取って、セキュリティ強度を高めるようにしてほしい。 |
実は、この現象はStruts 1.2.8以前のバージョンに存在する既知の脆弱性として報告されている問題である。
| 【参考URL】 http://struts.apache.org/struts-doc-1.2.9/userGuide/release-notes.html http://www.ipa.go.jp/security/vuln/documents/2006/JVN_72225922_struts.html http://jvn.jp/jp/JVN%2372225922/ |
org.apache.struts.taglib.html.CANCELをリクエストパラメータに付加するだけ、という容易な手法で攻撃が成功するため、危険度は高い。対策が必須な問題であるといえるだろう。
入力値チェックが回避される脆弱性対策
影響を受けるアプリケーション
IPAの解説にあるように、入力値チェックが回避されてしまうこの問題は、Validatorを実装したすべてのアプリケーションに影響する。Cancel機能による入力値チェックの回避で例を挙げたように、Cancelタグを使用していないから関係ない、という問題ではなく、Action#isCancelledメソッドによるCancel状態のチェックを行っていないアプリケーションであれば影響が及ぶ。
バージョンアップができる場合の対策
バージョンアップが可能な環境なら、Struts 1.2.9、Struts 1.3.0へバージョンアップすることにより、問題が解決できる。
バージョンアップができない場合の対策
対策が必須とはいえ、バージョンアップが難しい環境もあるだろう。以下に対策例をいくつか挙げるので、実現可能な手法を選択し、何らかの対策を取ることを検討していただきたい。
1.すべてのActionクラスでisCanceledメソッドを呼び出し、チェックする
すべてのActionクラスでisCanceledメソッドを呼び出し、チェックするように実装すれば、問題を解決することが可能である。Cancel機能を使用しないアプリケーションにおいてtrueが返されたら、特定のページに遷移させるように実装すればよい。
2.ベースActionクラスを作成し、Cancel時のデフォルト動作を宣言しておく
あらかじめ基底Actionクラスを使用しているような環境ならば、基底Actionクラス内で、protectedメソッドとして、Cancel時のデフォルト動作を宣言しておけばよいだろう。1.と同じくCancel機能を使用しないアプリケーションにおいてtrueが返されたら、特定のページに遷移させるように実装すればよい。
3.RequestProceserを書き換えてCancel機能を無効にする
Cancel機能はDispatchActionの仕組みで代替実装が可能なため、Cancel機能自体を使用していない環境も多いだろう。その場合は、RequestProceserをカスタマイズしてCancel機能を無効にしてしまうことでも、対策することが可能だ。
Cancel機能を無効にするための実装例を以下に記述する。
(1)struts-config.xmlのcontroller要素で設定されているRequestProcessorのサブクラスを作成し、processValidateメソッドをオーバーライドする。
| public class CustomRequestProcessor extends TilesRequestProcessor { protected boolean processValidate(HttpServletRequest request, HttpServletResponse response, ActionForm form, ActionMapping mapping) throws IOException, ServletException { //cancel機能を無効化 request.setAttribute(Globals.CANCEL_KEY, null); return super.processValidate(request, response, form, mapping); } } |
| TilesRequestProcessorを使っていた場合 |
(2)作成したクラスを使用するように、struts-config.xmlを修正する。
| <controller processorClass="org.apache.struts.tiles.TilesRequestProcessor"/> |
↓ |
| <controller processorClass="jp.mbsd.action.CustomRequestProcessor"/> |
| TilesRequestProcessorを上記で作成したCustomRequestProcessorに置き換える |
以上、Struts 1.2.8以前のバージョンに存在する脆弱性を中心にValidatorプラグインを使用する際の注意点について解説した。繰り返しになるが、大きな危険を持つ脆弱性であるため必ず対策を取るようにしていただきたい。
http://www.atmarkit.co.jp/fsecurity/rensai/struts03/struts01.html



