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 のドキュメントの説明を見ると以下のように書いてあります。
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