💻

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

2022/01/01に公開

はじめに

SPSecurity.RunWithElevatedPrivileges メソッドを使うときに出てくる SPWeb.AllowUnsafeUpdates プロパティについてです。このプロパティをエラー回避のために何も考えずに使っていることはよくあるのですが、実際には何をしているのかを改めて検証してみます。

サンプル コード

空のファーム ソリューション プロジェクトを作成します。視覚的 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 プロパティについて Microsoft Docs の説明を見ると以下のように書いてあります。

https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.sharepoint.spweb.allowunsafeupdates?WT.mc_id=M365-MVP-5002941

GET 要求の結果として、あるいは、セキュリティ検証を要求せずに、データベースに対する更新を許可するかどうかを指定するセキュリティ ブール値を取得または設定します。このプロパティを true に設定することには、セキュリティ上に問題があります。場合によってはクロスサイト スクリプティングの脆弱性が生じる可能性があります。

SharePoint では外部からの攻撃を回避するために、ダイジェストを HTML ページに埋め込んでいます。これは、FormDigest コントロールによって以下の HTML タグとして出力されます。

<input type="hidden" name="__REQUESTDIGEST" id="__REQUESTDIGEST" value="{{digest-value}}" />

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

回避策

回避策として 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);
        }
    });
}

Discussion