からめもぶろぐ。

SharePoint が得意なフレンズなんだね!すごーい!

SPWeb.AllowUnsafeUpdates は使うべきではない

SPSecurity.RunWithElevatedPrivileges メソッドを使うときに必ず一緒にお世話になる SPWeb.AllowUnsafeUpdates プロパティですが、エラー回避のために何も考えずに使っている例が多々あると思います。何も考えずに使っているこのコードが何をしているのかを改めて検証してみたいと思います。
なお、今回の記事のオチは MSDN Blog に書いてありますので、結論だけ知りたい方はこちらを参照してください。

blogs.msdn.com

サンプル コード

空のファーム ソリューション プロジェクトを作成します。視覚的 Web パーツを追加して、ボタンを配置します。クリック イベントで以下のコードを記述します。

protected void Button1_Click(object sender, EventArgs e) {
    var siteId = SPContext.Current.Site.ID;
    var webId = SPContext.Current.Web.ID;
    SPSecurity.RunWithElevatedPrivileges(() => {
        var fileText = "テスト";
        var fileName = "テスト.txt";
        var listName = "ドキュメント";
        using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(fileText)))
        using (var site = new SPSite(siteId))
        using (var web = site.OpenWeb(webId)) {
            var list = web.Lists[listName];
            var folder = list.RootFolder;
            web.AllowUnsafeUpdates = true;
            folder.Files.Add(fileName, stream, true);
            web.AllowUnsafeUpdates = false;
        }
    });
}

このコードではドキュメント ライブラリにファイルをアップロードします。ドキュメント ライブラリに投稿権限がない場合でもアップロードできるように、RunWithElevatedPrivileges メソッドによって権限を昇格しています。アップロードする処理の直前に AllowUnsafeUpdates = true を入れることで、エラーの発生を防いでいます。

問題点

AllowUnsafeUpdates プロパティについて MSDN の説明を見ると以下のように書いてあります。

SPWeb.AllowUnsafeUpdates プロパティ (Microsoft.SharePoint)

GET 要求の結果として、あるいは、セキュリティ検証を要求せずに、データベースに対する更新を許可するかどうかを指定するセキュリティ ブール値を取得または設定します。

このプロパティを true に設定することには、セキュリティ上に問題があります。場合によってはクロスサイト スクリプティングの脆弱性が生じる可能性があります。

この説明なのですが、クロスサイト スクリプティングじゃなくてクロスサイト リクエスト フォージェリの間違いだと思います。つまり、SharePoint ではクロスサイト リクエスト フォージェリによる外部からの攻撃を回避するために、ダイジェストを HTML ページに埋め込んでいます。これは、FormDigest コントロールによって以下の HTML タグとして出力されます。

<input type="hidden" name="__REQUESTDIGEST" id="__REQUESTDIGEST" value="<ダイジェスト値>" />

ここから送られるダイジェスト値をサーバー側で検証しています。おそらくですが、ダイジェスト値の検証にログイン ユーザー情報が関係しているため、RunWithElevatedPrivileges メソッドによってユーザーが変わるとエラーになるのではないかと思います。そこで、AllowUnsafeUpdates プロパティによって検証をしないようにすれば、エラーを回避できるわけです。ということは、セキュリティの脆弱性は残ったままですね。これは問題です。

回避策

回避策として、先の MSDN Blog では SPUtility.ValidateFormDigest メソッドを使うことが提案されています。

protected void Button1_Click(object sender, EventArgs e) {
    var siteId = SPContext.Current.Site.ID;
    var webId = SPContext.Current.Web.ID;

    /* このコードを追加 */
    SPUtility.ValidateFormDigest();

    SPSecurity.RunWithElevatedPrivileges(() => {
        var fileText = "テスト";
        var fileName = "テスト.txt";
        var listName = "ドキュメント";
        using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(fileText)))
        using (var site = new SPSite(siteId))
        using (var web = site.OpenWeb(webId)) {
            var list = web.Lists[listName];
            var folder = list.RootFolder;
            folder.Files.Add(fileName, stream, true);
        }
    });
}

ユーザーが昇格する前に検証を明示的に実行します。実行結果はキャッシュされるので、以降の処理ではエラーが発生しません。脆弱性も回避できています。素晴らしいですね。

おまけ

SPSite.OpenWeb メソッドの呼び出し前であれば RunWithElevatedPrivileges メソッドのデリゲートの中でも大丈夫なようです。

protected void Button1_Click(object sender, EventArgs e) {
    var siteId = SPContext.Current.Site.ID;
    var webId = SPContext.Current.Web.ID;
    SPSecurity.RunWithElevatedPrivileges(() => {
        var fileText = "テスト";
        var fileName = "テスト.txt";
        var listName = "ドキュメント";

        /* このコードを追加 */
        SPUtility.ValidateFormDigest();

        using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(fileText)))
        using (var site = new SPSite(siteId))
        using (var web = site.OpenWeb(webId)) {
            var list = web.Lists[listName];
            var folder = list.RootFolder;
            folder.Files.Add(fileName, stream, true);
        }
    });
}