インジェクションアタックに対するセキュリティ対策のため、ASP.NET 2.0 からコントロールにイベント検証の機能が追加されています。ポストバック処理の際、クライアントから POST された値を検証し、未知の値の場合は以下のように ArgumentException をスローします。
無効なポストバックまたはコールバック引数です。イベントの検証は、構成の <pages enableEventValidation="true"/>、またはページの <%@ Page EnableEventValidation="true" %> を使用して有効にされます。セキュリティの目的により、この機能は、イベントをポストバックまたはコールバックする引数が、それらを最初に表示したサーバー コントロールから発行されていることを確認します。データが有効であり、予期されている場合、検証のためのポストバックまたはコールバック データを登録するために ClientScriptManager.RegisterForEventValidation メソッドを使用してください。
ASP.NET のすべてのイベント ドリブン コントロールは、既定でこの機能を使用するそうです。実は、普通にコントロールを使用しているときは当然検証は通るので、このような機能があることは知らなかったです。(汗)
例えば以下のコードのように、クライアントスクリプトで DropDownList に ListItem(HTML では option)を追加し、追加した項目(以下のコードの例では Item-3)を選んでからポストバックすると、上記の例外がスローされるのが分かります。
<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>EnableEventValidation</title>
<script type="text/javascript">
//<![CDATA[
function AddItemToList() {
var d = document.getElementById('<%=ddl.ClientID%>');
d.options[2] = new Option('Item-3', '3');
}
//]]>
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:DropDownList ID="ddl" runat="server">
<asp:ListItem Value="1">Item-1</asp:ListItem>
<asp:ListItem Value="2">Item-2</asp:ListItem>
</asp:DropDownList>
<input type="button"
id="btn1"
value="Add Item-3"
onclick="javascript:AddItemToList();" />
<br />
<asp:Button ID="Button1"
runat="server"
Text="PostBack" />
</div>
</form>
</body>
</html>
どのように検証しているかと言うと、ASP.NET はコントロールの UniqueID とポストされる可能性のある値を以下のように隠しフィールドに格納し、ポストバックされた際に、ポストされた値と隠しフィールドの値を比較するという操作を行っています。上の例では、value の "1" と "2" は隠しフィールドにありますが、"3" はないので検証 NG となって例外がスローされます。
<input type="hidden"
name="__EVENTVALIDATION"
id="__EVENTVALIDATION"
value="/wEWBALBxKbEBwLVoIS......." />
この例外を避けるには、以下の方法があります。
-
@ Page ディレクティブに EnableEventValidation="false" を追加する。
-
ClientScriptManager.RegisterForEventValidation メソッドで検証用のイベント参照を登録する。
上記 1 の方法では、ページ全体でイベント検証が無効になってしまいますので、そのページで意図しない影響を与える可能性があるポストバックが一切発生しないことが条件になります。
上記 2 の方法では、イベント検証をパスするように、あらかじめイベント参照(上記のコードの例では DropDownList の UniqueID と Item-3 の value である "3")を登録します。
RegisterForEventValidation メソッドはレンダリングのタイミングで呼ぶ必要があるそうです。従って、Render メソッドを override してその中で設定します。
今回の例の場合、Page の Render メソッドを override するより、DropDownList を継承したカスタムコントロールを作って、その Render メソッドを override した方がよさそうです。以下のような感じです。
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI;
namespace EventValidationTest
{
// カスタムコントロールは、SupportsEventValidationAttribute
// 属性を定義しないと、イベントの検証に含まれません。
[SupportsEventValidation]
public class MyDropDownList : DropDownList
{
// RegisterForEventValidation メソッドを使用して、
// UniqueID と POST される value を登録する(具体的に
// は、隠しフィールド __EVENTVALIDATION の value に追
// 加する)。
protected override void Render(HtmlTextWriter writer)
{
Page.ClientScript.RegisterForEventValidation(
this.UniqueID,
"3"
);
base.Render(writer);
}
// 以下はオマケ。
// クライアントスクリプトで、DropDownList に動的に追加し
// た ListItem (option) は、ポストバック後に再描画された
// とき消えてしまう。以下のコードで再生できる。
protected override bool LoadPostData(string postDataKey,
NameValueCollection postCollection)
{
string postedValue = postCollection[postDataKey];
if (postedValue != null &&
this.Items.FindByValue(postedValue) == null)
{
this.Items.Add(
new ListItem("Item-" + postedValue, postedValue));
}
return base.LoadPostData(postDataKey, postCollection);
}
}
}
ただし、上記 2 の対応が常に可能ではないところが問題です。この例では value があらかじめ分かってないと登録できません。value が確定できない場合は、イベント検証を無効にするより手がありません。しかし、ページ全体のイベント検証を無効にしたくはないですよね。
問題のコントロールのみイベント検証を無効にするには、そのコントロールを継承したカスタムコントロールを作り、SupportsEventValidationAttribute 属性を定義しないことで実現できます。
なお、イベント検証は、ポストバックの時のみでなく、クライアントコールバックの時でも有効にできます。詳しくは MSDN ライブラリの ValidateEvent メソッド を見てください。