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 プロパティを 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