🔒

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

に公開

はじめに

今回の記事では、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 のドキュメントでは以下のように説明されています。

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 プロパティを true に設定して検証を回避できますが、セキュリティの脆弱性が残ったままとなります。これは問題です。

回避策

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

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

おわりに

RunWithElevatedPrivileges メソッドのデリゲート内でも SPUtility.ValidateFormDigest を実行できます。

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