WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

EnableEventValidation

by WebSurfer 2012年1月15日 15:19

インジェクションアタックに対するセキュリティ対策のため、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......." />

この例外を避けるには、以下の方法があります。

  1. @ Page ディレクティブに EnableEventValidation="false" を追加する。
  2. 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 メソッド を見てください。

Tags:

Validation

textarea のキャレット位置に文字列を挿入

by WebSurfer 2012年1月9日 23:04

JavaScript を使って textarea のキャレット(カーソル)位置に文字列を挿入するサンプルです。

textarea のキャレット位置に文字列を挿入

挿入する文字列を入力するために jQuery UI の dialog を利用していますが、それ以外は JavaScript のみを使用しています。

Firefox などの場合は、選択された部分の先頭、末尾の index (整数型)を、それぞれ selectionStart、selectionEnd で取得できますが、IE の場合はそれに該当するプロパティがないのが問題です。

IE の場合は、document.selection プロパティで selection オブジェクト(textarea 要素そのものではなく、textarea 要素の中の選択された文字の部分)を取得し、さらに createRange メソッドを使って TextRange オブジェクトを作成し、それを操作することになります。

TextRange オブジェクトについては、MSDN ライブラリの TextRangeオブジェクトの使用 を参照してください。

そのサンプルコードは以下の通りです。上の画像は、このコードを実行したときのブラウザの画面です。実際に動かして試せるよう 実験室 にアップしましたので、興味のある方は試してみてください。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>textarea のキャレット位置に文字列を挿入</title>
  <script src="Scripts/jquery-1.6.2.min.js" type="text/javascript"></script>
  <script src="Scripts/jquery-ui-1.8.16.custom.min.js" type="text/javascript"></script>
  <link href="css/smoothness/jquery-ui-1.8.16.custom.css" rel="stylesheet" type="text/css" />
  <script type="text/javascript">
  //<![CDATA[

    // IE の場合、[ダイアログ表示]ボタンクリックの時点で
    // textarea 内の選択された文字部分の TextRange オブジェ
    // クトを取得して維持しておく。そうしないと、textbox に
    // 入力中に textarea からフォーカスが外れ、選択範囲がリ
    // セットされてしまう。
    // Firefox 等は textarea からフォーカスが外れても、選択
    // 範囲は維持されるので、このような処置は不要。
    var textRange = null;

    function getTextRange(textarea) {
      // textarea の文字が選択されてない場合はフォーカスを
      // 当てないとうまくいかない。
      textarea.focus();

      // document.selection プロパティで selection オブジ
      // ェクト(testarea の中の選択された文字の部分)を取
      // 得する。文字が選択されていない場合、キャレット位
      // 置の空の selection オブジェクトになる。
      // selection オブジェクトに何か処理を行う場合は、
      // createRange メソッドで TextRange オブジェクトを作
      // 成して処理する。
      return document.selection.createRange();
    }

    function insertText(textarea, txt) {
      if (textRange == null) {
        // IE 以外の場合

        // 選択部分の先頭の index と長さを取得
        var index = textarea.selectionStart;
        var length = textarea.selectionEnd - index;

        // 文字列を挿入
        textarea.value = textarea.value.substr(0, index) +
          txt + textarea.value.substr(index + length);

        // キャレット位置を挿入した文字列の最後尾に移動
        textarea.focus();
        var newCaretPosition = index + txt.length;
        textarea.setSelectionRange(
          newCaretPosition, newCaretPosition);
      } else {
        // IE の場合

        // TextRange.text プロパティにダイアログの文字列を
        // 設定し、testarea の中の選択された文字部分を置き
        // 換える。
        textRange.text = txt;

        // キャレット位置を挿入した文字列の最後尾に移動
        textRange.select();
      }
    }

    // ---------------------------------------------------
    // 挿入する文字列の入力用に jQuery UI の dialog を利用
    $(function () {
      $("#dialog").dialog({
        bgiframe: true,
        autoOpen: false,
        modal: true,
        resizable: false,
        buttons: {
          'Insert': function () {
            insertText(
              $('#textarea').get(0),
              $('#textbox').val()
            );
            $(this).dialog('close');
          },
          'Cancel': function () {
            $(this).dialog('close');
          }
        },
        open: function () {
          $('#textbox').val('')
        }
      });
      $('#buttonShowDialog').click(function () {
        if (document.selection != undefined) {
          textRange = getTextRange($('#textarea').get(0));
        }
        $('#dialog').dialog('open');
      });
    });
  //]]>
  </script>
</head>
<body style="font-size: 12px;">
  <h1>キャレット位置に文字列を挿入</h1>
  <textarea id="textarea" cols="50" rows="5">textarea のキャレット位置に文字列を挿入します。</textarea><br />
  <button id="buttonShowDialog">ダイアログ表示</button>

  <div id="dialog" title="挿入する文字列">
    <p>ここに入力:<br />
    <input type="text" id="textbox" size="30" /></p>
  </div>
</body>
</html>

textarea の文字列が選択されている場合、その文字列を dialog に入力した文字列で置き換えます。文字列を選択していない場合(キャレットのみの場合)、キャレットの位置に dialog に入力した文字列を挿入します。

IE9, Firefox 9.0.1, Safari 5.1.2, Opera 11.60 で検証して期待通りに動くことを確認しました。(その他のブラウザは検証してないです)

Tags: ,

JavaScript

TableAdapterManager

by WebSurfer 2011年12月21日 22:19

Visual Studio には、型指定された DataSet + TableAdapter をウィザードベースで自動生成する機能があります。さらに、Visual Studio 2008 以降では、階層更新を実現する TableAdapterManager クラスが追加で自動生成されるようになりました。

今さらながらですが、TableAdapterManager について調べて、いろいろ発見があったので忘れないように書いておきます。

TableAdapterManager を利用したアプリケーション

階層更新とは、簡単に言うと、複数のテーブルで構成される DataSet が持つ更新データを、データベースの整合性に関する規則(参照整合性規則)を守って、データベースに挿入、更新、削除するプロセスのことです。(詳しい説明は MSDN ライブラリの 階層更新 を参照してください)

例として、上の画像のような、Northwind サンプルデータベースの Customers と Orders テーブル両方を同時に管理するアプリケーションを考えます。(Customers テーブルは CustomerID を主キーとして持ち、Orders テーブルは CustomerID を外部キーとして持っています。)

新しい顧客から注文があった場合は、先に新しい顧客レコードを Customers テーブルに Insert してから、注文レコードを Orders テーブルに Insert しなければなりません。

Customers テーブルから顧客レコードを Delete する場合は、先に Orders テーブルの当該顧客の注文レコードをすべて Delete してからにしなければなりません。

さらに、Insert、Update、Delete の順序も重要です。例えば、Customers テーブルの既存のレコードの主キー値を変更する場合、以下のような手順にしなければなりません。

  1. Customers テーブルに、新しい CustomerID 値で、新しいレコードを Insert
  2. Orders テーブルの当該レコードの CustomerID を新しい値に Update
  3. Customers テーブルの旧 CustomerID のレコードを Delete

従って、Insert => Update => Delete の順で行う必要があります(TableAdapterManager のデフォルトがこれ。他に、Update => Insert => Delete とすることも可能)。

Visual Studio 2005 以前のバージョンでは、上記のような参照整合性規則を守って更新を行う(即ち、階層更新を実現する)ためのコードを自力で実装する必要がありました。具体的には以下ようなコードを実装しなければなりません。

  1. まず、DataTable の Select メソッドを使用して特定の RowState(Added、ModifiedCurrent、Deleted)を持つ行だけを参照する DataRow 配列を取得します。(DataAdapter によるデータ ソースの更新 の「挿入、更新、削除の順序」のセクション参照)
  2. その後、各テーブルの TableAdapter の Update(DataRow[]) メソッドを適切な順序で呼び出し、各テーブルから RowState 別に取得した DataRow 配列を適切な順序で渡して処理します。
  3. 当然ながらトランザクション処理も必要です。

Visual Studio 2008 から新しく追加された TableAdapterManager クラスには、階層更新を実現するロジックを持つ UpdateAll メソッドが実装されています。MS-DTC を使わない手動トランザクション処理も実装されています。

実際に、MSDN ライブラリのチュートリアル SQL Server Express データベースの作成SQL Server Express データベース内のデータへの接続 (Windows フォーム) を参考に、型指定された DataSet + TableAdapter を作って、その中の TableAdapterManager のコードを見てみました。(もっと詳しいチュートリアルがありました。下の 2011/12/22 追記の「その1」を参照ください)

TableAdapterManager の UpdateAll メソッドは、Customers => Orders の順で両方のテーブルに対して Insert を行い、次に Customers => Orders の順で両方のテーブルに対して Update を行い、そして、最後に Orders => Customers の順で両方のテーブルに対して Delete を行うというように、上に述べた参照整合性規則を守って更新を実行するためのコードが実装されているのが確認できました。

TableAdapterManager は万能ではないと思いますが、多くのケースで、自力で階層更新を実現するためのコードを書く必要がなくなったのではないでしょうか。

自動生成されたコードで対応できない場合は、TableAdapterManager クラスは partial として定義されているので、自力でコードを書いてそれを拡張すれば、Visual Studio 2005 以前のときと比べて比較的簡単に対応可能と思います。

------------- 2011/12/22 追記 -------------

その1

Customers テーブルと Orders テーブルを使ったアプリケーションの作成は、上記のページより、10 行でズバリ !! 非接続型のデータ アクセス (ADO.NET) (C#) の方が詳しく書いてあって、参考になると思います。

その2

単一テーブルのアプリケーションにおいて、既存のレコードを Delete して、そのレコードと同じ主キーを持つレコードを新たに Insert する場合、Delete => Insert の順でないとうまくいきません。

TableAdapterManager は、Insert => Update => Delete(デフォルト)または Update => Insert => Delete のいずれかしか更新順序は選べませんので、上記のケースには対応できないということになります。

本文に述べた Customers テーブルの既存のレコードの主キー値を変更するケースより、上記のケースの方が多そうな気がするのですが、Microsoft は何故 Delete を最初にしなかったのでしょう? 気になります。(2015/11/12 追記: Delete => Insert などという乱暴なことを許すと、ユーザーのミスで問題が出る可能性が高くなるからではないかと最近思い初めています。Update すれば済む話ですから)

Tags:

ADO.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar