到目前为止,在我的研究中,我发现在GET请求操作上设置AllowUnsafeUpdates是不明智的,以避免跨站点脚本。但是,如果要求允许这样做,处理这种情况以减轻任何暴露的正确方法是什么?
如果您绝对需要在GET请求中允许网站或网站更新,这是我对可靠模式的最佳猜测。
最佳实践?
protected override void OnLoad(System.EventArgs e)
{
if(Request.HttpMethod == "POST")
{
SPUtility.ValidateFormDigest();
// will automatically set AllowSafeUpdates to true
}
// If not a POST then AllowUnsafeUpdates should be used only
// at the point of update and reset immediately after finished
// NOTE: Is this true? How is cross-site scripting used on GET
// and what mitigates the vulnerability?
}
// Point of item update
using(SPSite site = new SPSite(SPContext.Current.Site.Url, SPContext.Current.Site.SystemAccount.UserToken))
{
using (SPWeb web = site.RootWeb)
{
bool allowUpdates = web.AllowUnsafeUpdates; //store original value
web.AllowUnsafeUpdates = true;
//... Do something and call Update() ...
web.AllowUnsafeUpdates = allowUpdates; //restore original value
}
}
赞赏对最佳模式的反馈。
答案 0 :(得分:7)
如果您正在执行任何修改某些操作的操作,那么任何可以说服用户点击链接的操作都可以执行该操作。例如,假设您对页面有一个GET请求,该页面允许用户将管理员添加到站点,并且用户单击指向执行Response.Redirect(“http://yourserver/_layouts/admin.aspx?operation=addAdministrator&username=attackerNameHere”)的页面的链接。
虽然POST通常不会对此提供太多保护(没有什么能阻止某人使用< form method =“post”action =“http://yourserver/_layouts/admin.aspx”>),SharePoint具有表单摘要的概念,其中包含有关生成回发的先前请求的信息(包括用户的名称)。这大大减少了这种攻击的足迹。
如果您没有从用户那里获取输入,那么唯一一次在GET上对AllowUnsafeUpdates不是安全问题。例如,如果您的Web部件也记录了对列表的访问,那么就不会暴露安全漏洞。
编辑:如果您要使用AllowUnsafeUpdates,则无需将其重置为之前的值。它不会持久化。这只是在从GET(或其他情况)执行更新之前需要在SPWeb对象上设置的东西
答案 1 :(得分:5)
我会略微修改Trent的委托以接受网页更新:
public static void DoUnsafeUpdate(this SPWeb web, Action<SPWeb> action)
{
try
{
web.AllowUnsafeUpdates = true;
action(web);
}
finally
{
web.AllowUnsafeUpdates = false;
}
}
然后扩展HttpContext以封装表单摘要的验证,并提供使用technique described here提升的选项:
public static void DoUnsafeUpdate(this HttpContext context, Action<SPWeb> action, bool elevated)
{
SPWeb web = SPControl.GetContextWeb(context);
if (!context.Request.HttpMethod.Equals("POST", StringComparison.Ordinal)
|| web.ValidateFormDigest())
throw new SPException("Error validating postback digest");
if (elevated)
web.RunAsSystem(w => w.DoUnsafeUpdate(action));
else
web.DoUnsafeUpdate(action);
}
用法:
protected override void OnLoad(System.EventArgs e)
{
Context.DoUnsafeUpdate(web =>
{
// Update elevated web
}, true);
}
答案 2 :(得分:4)
另一种实现的简洁方法是使用扩展方法和匿名委托的组合:
public static void DoUnsafeUpdate(this SPWeb web, Action action)
{
bool allowUnsafeUpdates = web.AllowUnsafeUpdates;
web.AllowUnsafeUpdates = true;
action();
web.AllowUnsafeUpdates = allowUnsafeUpdates;
}
使用上述扩展方法,您可以按如下方式执行“不安全更新”操作:
var web = SPContext.Current.Web;
web.DoUnsafeUpdate(delegate()
{
// Put your "unsafe update" code here
});
答案 3 :(得分:2)
对于AllowUnsafeUpdates,我遵循以下过程:
if( HttpContext.Current is null )
{
Do nothing, no need to set AllowUnsafeUpdates to true nor
to call ValidateFormDigest() because update will be carried out
}
else // HttpContext.Current is NOT null
{
if( SPContext.Current is null )
{
Need to set AllowUnsafeUpdates to true
}
else // SPContext.Current is NOT null
{
Call ValidateFormDigest()
}
}
答案 4 :(得分:1)
不太确定是否值得记住允许不安全更新的先前值。
我希望围绕尽可能少的代码包装调用,以便不会发生对它的嵌套调用。
然后你可以在之后把它变成假。
答案 5 :(得分:1)
我使用包装类来处理SPWeb对象的大多数操作。这有助于我记得关闭网页,它可以减轻不安全更新设置的问题。它有点臃肿,因为我修补了新的构造函数和成员。但话又说回来; SPWeb类也是如此。
用法:
using (WebWrapper wrapper = new WebWrapper("http://localhost"))
{
wrapper.AllowUnsafeUpdates();
//Do work on wrapper.
}
班级定义:
using System;
using System.Collections.Specialized;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.Serialization;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
namespace Skaar.SharePoint.Customization
{
/// <summary>
/// A wrapper for a <see cref="SPWeb"/> object.
/// <remarks>Closes web object on Dispose if applicable.</remarks>
/// </summary>
[Serializable]
[DebuggerDisplay("{Uri} Unsafe:{AllowUnsafeUpdatesSetting} Update:{UpdatePending}")]
public sealed class WebWrapper : IDisposable, IDeserializationCallback, IEquatable<WebWrapper>
{
[NonSerialized] private bool unsafeUpdatesSetting;
[NonSerialized] private SPWeb web;
/// <summary>
/// Determines if the inner web object should be closed.
/// </summary>
[NonSerialized] private bool webShouldBeClosed;
/// <summary>
/// This value is used in serialization to restore <see cref="Web"/>.
/// </summary>
private string webUrl;
/// <summary>
/// Creates a new wrapper object.
/// </summary>
/// <param name="web">A web that should be closed/disposed when done.</param>
public WebWrapper(SPWeb web) : this(web, true)
{
}
/// <summary>
/// Creates a new wrapper object.
/// </summary>
/// <param name="web">An inner web object</param>
/// <param name="webShouldBeClosed">If true, the web object is closed in the <see cref="Dispose()"/> method.</param>
public WebWrapper(SPWeb web, bool webShouldBeClosed)
{
setWeb(web, webShouldBeClosed);
}
/// <summary>
/// Creates a new wrapper object.
/// </summary>
/// <param name="webAddress">The address to a web.</param>
public WebWrapper(Uri webAddress)
{
using (SPSite site = new SPSite(webAddress.ToString()))
{
string relativeUrl = renderWebRootRelativeUrl(webAddress);
if (relativeUrl == null)
{
setWeb(site.OpenWeb(), true);
}
else
{
setWeb(site.OpenWeb(relativeUrl), true);
}
}
}
private string renderWebRootRelativeUrl(Uri address)
{
for (int i = 0; i < address.Segments.Length; i++)
{
string segment = address.Segments[i];
if (string.Equals(segment, "_layouts/"))
{
string newUrl=string.Join(null, address.Segments, 0, i).Trim('/');
return newUrl;
}
}
return null;
}
/// <summary>
/// If true, <see cref="SPWeb.Update"/> will be called in <see cref="Dispose()"/>.
/// </summary>
public bool UpdatePending { get; private set; }
/// <summary>
/// The setting of the inner web (<see cref="SPWeb.AllowUnsafeUpdates"/>)
/// </summary>
public bool AllowUnsafeUpdatesSetting
{
get { return Web.AllowUnsafeUpdates; }
}
/// <summary>
/// The inner object.
/// </summary>
/// <exception cref="ObjectDisposedException">Exception is thrown if <see cref="IsDisposed"/> is true.</exception>
public SPWeb Web
{
get
{
if(IsDisposed)
{
throw new ObjectDisposedException("Web wrapper is disposed.");
}
return web;
}
}
/// <summary>
/// The address of the <see cref="Web"/> wrapped as a <see cref="Uri"/> object.
/// </summary>
public Uri Uri
{
get { return new Uri(Web.Url); }
}
/// <summary>
/// The address of the <see cref="Web"/> wrapped as a <see cref="Uri"/> object.
/// </summary>
public Uri GetUri(SPUrlZone zone)
{
return Site.WebApplication.GetResponseUri(zone, Uri.AbsolutePath);
}
/// <summary>
/// Creates a wrapper around the context web.
/// <remarks>The web will not be closed when wrapper is disposed. Returns null if context is unavailable.</remarks>
/// </summary>
public static WebWrapper Context
{
get
{
return SPContext.Current==null?null:new WebWrapper(SPContext.Current.Web, false);
}
}
/// <summary>
/// This is a static property wrapping of
/// the <see cref="CloneOf(SPWeb)"/> method, using
/// the <see cref="SPContext"/> current web as
/// parameter.
/// <remarks>Returns null if context is unavailable.</remarks>
/// </summary>
public static WebWrapper CloneOfContext
{
get
{
if (SPContext.Current != null)
{
SPWeb contextWeb = SPContext.Current.Web;
return CloneOf(contextWeb);
}
return null;
}
}
/// <summary>
/// Returns the <see cref="SPWeb.Exists"/> property of the <see cref="Web"/> object.
/// </summary>
public bool Exists
{
get { return Web != null && Web.Exists; }
}
/// <summary>
/// Gets the <see cref="SPSite"/> object of <see cref="Web"/>.
/// </summary>
/// <remarks>This object should not be closed by user code.</remarks>
public SPSite Site
{
get { return web.Site; }
}
/// <summary>
/// Gets the owner defined in <see cref="SPSite.Owner"/>.
/// </summary>
public SPUser Owner
{
get
{
return Site.Owner;
}
}
/// <summary>
/// Returns a context of the inner <see cref="Web"/>.
/// </summary>
public SPContext ContextOfWeb
{
get { return SPContext.GetContext(web); }
}
/// <summary>
/// Gets the language of <see cref="Web"/>.
/// </summary>
public CultureInfo Locale
{
get { return Web.Locale; }
}
/// <summary>
/// Gets the language of the root web.
/// </summary>
public CultureInfo LocaleOfRoot
{
get
{
using (WebWrapper root = Root)
{
return root.Locale;
}
}
}
/// <summary>
/// Returns a new <see cref="WebWrapper"/> wrapping the root <see cref="SPWeb"/> of this.
/// </summary>
public WebWrapper Root
{
get
{
if (webShouldBeClosed)
using (SPSite site = Site)
{
return new WebWrapper(site.RootWeb);
}
return new WebWrapper(Site.RootWeb);
}
}
/// <summary>
/// A wrapper for <see cref="SPWeb.Title"/>.
/// </summary>
public string Title
{
get { return Web.Title; }
set { Web.Title = value; }
}
/// <summary>
/// A wrapper for <see cref="SPWeb.ID"/>.
/// </summary>
public Guid ID
{
get { return Web.ID; }
}
#region Web Properties
[NonSerialized] private bool updatePropertiesPending;
/// <summary>
/// A wrapper method to <see cref="Web"/> object's <see cref="SPWeb.Properties"/> indexer.
/// </summary>
/// <param name="key">The key to use when fetching property value.</param>
/// <returns>A string containing the value.</returns>
public string GetProperty(string key)
{
return Web.Properties[key];
}
/// <summary>
/// Sets the value in the <see cref="Web"/> object's <see cref="SPWeb.Properties"/>. Creates a new key, or updates an existing as needed.
/// </summary>
/// <param name="key">The key to use when storing the property value.</param>
/// <param name="value">The value to set in the key.</param>
/// <remarks>The property <see cref="UpdatePending"/> is set to true.</remarks>
public void SetProperty(string key, string value)
{
if (!Web.Properties.ContainsKey(key))
{
Web.Properties.Add(key, value);
}
else
{
Web.Properties[key] = value;
}
updatePropertiesPending = true;
}
#endregion
#region IDeserializationCallback Members
///<summary>
///Runs when the entire object graph has been deserialized.
///</summary>
///
///<param name="sender">The object that initiated the callback. The functionality for this parameter is not currently implemented. </param>
public void OnDeserialization(object sender)
{
using (SPSite site = new SPSite(webUrl))
{
setWeb(site.OpenWeb(), true);
}
}
#endregion
#region IDisposable Members
///<summary>
///Closes inner web object if appropriate.
///</summary>
///<filterpriority>2</filterpriority>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool isDisposing)
{
if (IsDisposed) return;
if (isDisposing)
{
doDisposeOfWeb();
IsDisposed = true;
}
}
#endregion
/// <summary>
/// Value is true if <see cref="Dispose()"/> method has been called. Object is not in a usable state.
/// </summary>
internal bool IsDisposed
{
get; private set;
}
#region IEquatable<WebWrapper> Members
/// <summary>
/// This tests whether the two objects wraps the same web. It may however be two different instances of the same web.
/// </summary>
/// <param name="other">Another wrapper object.</param>
/// <returns>True if <see cref="Uri"/> equals, false otherwise.</returns>
public bool Equals(WebWrapper other)
{
if (other == null)
{
return false;
}
return Uri.Equals(other.Uri);
}
#endregion
/// <summary>
/// Reopens the inner <see cref="SPWeb"/> object. May be used when web object needs to be rereferenced in a new security context.
/// </summary>
public void ReOpen()
{
bool unsafeSetting = AllowUnsafeUpdatesSetting;
using (SPSite site = new SPSite(Web.Url))
{
SPWeb newWeb = site.OpenWeb();
doDisposeOfWeb();
web = newWeb;
web.AllowUnsafeUpdates = unsafeSetting;
unsafeUpdatesSetting = false;
webShouldBeClosed = true;
}
}
private void doDisposeOfWeb()
{
if (Web == null) return;
Update(true);
if (webShouldBeClosed)
{
Web.Close();
}
else if (Web.Exists)
{
Web.AllowUnsafeUpdates = unsafeUpdatesSetting;
}
web = null;
}
/// <summary>
/// Calls <see cref="SPWeb.Update"/> on the <see cref="Web"/> object.
/// </summary>
public void Update()
{
Update(false);
}
/// <summary>
/// Sets <see cref="UpdatePending"/> to <c>true</c>.
/// </summary>
public void SetUpdatePending()
{
UpdatePending = true;
}
/// <summary>
/// Calls <see cref="SPWeb.Update"/> on the <see cref="Web"/> object.
/// <param name="onlyIfPending">If true, update will depend on state of the <see cref="UpdatePending"/> property.</param>
/// </summary>
public void Update(bool onlyIfPending)
{
if (onlyIfPending)
{
if (updatePropertiesPending)
{
Web.Properties.Update();
updatePropertiesPending = false;
}
if (UpdatePending)
{
Web.Update();
UpdatePending = false;
}
}
else
{
Web.Update();
UpdatePending = false;
}
}
/// <summary>
/// Returns the list from <see cref="Web"/> with <see cref="SPList.Title"/> equal to <see cref="title"/>.
/// </summary>
/// <param name="title">The <see cref="SPList.Title"/> of an existing list.</param>
/// <returns>The first list found with the given title, or null, if no list is found.</returns>
public SPList GetList(string title)
{
foreach (SPList list in Web.Lists)
{
if (list.Title == title)
{
return list;
}
}
return null;
}
/// <summary>
/// A wrapper method to the <see cref="Web"/> object's <see cref="SPWeb.Lists"/> indexer.
/// </summary>
/// <param name="id">The id of the list to return.</param>
/// <returns>The list with the supplied id.</returns>
public SPList GetList(Guid id)
{
return Web.Lists[id];
}
private void setWeb(SPWeb innerWeb, bool shouldBeClosed)
{
if (innerWeb == null || !innerWeb.Exists)
{
throw new ArgumentException("Web does not exist", "innerWeb");
}
web = innerWeb;
webShouldBeClosed = shouldBeClosed;
unsafeUpdatesSetting = innerWeb.AllowUnsafeUpdates;
AllowUnsafeUpdates();
webUrl = web.Url;
}
/// <summary>
/// Creates a new <see cref="SPWeb"/> object using the
/// url of the <see cref="web"/> parameter and wraps it
/// in a new wrapper object. The web will be
/// closed when the wrapper is disposed.
/// The cloning is done using the <see cref="SPWeb.Url"/>, thus no security context is transferred to the new web.
/// </summary>
/// <remarks>Use this to create a clone of the context web.</remarks>
/// <param name="web">The web to clone.</param>
/// <returns>A new wrapper object.</returns>
public static WebWrapper CloneOf(SPWeb web)
{
using (SPSite site = new SPSite(web.Url))
{
return new WebWrapper(site.OpenWeb());
}
}
/// <summary>
/// Creates a new <see cref="SPWeb"/> object using the
/// <see cref="Web"/> of the <see cref="web"/> parameter and wraps it
/// in a new wrapper object. The web will be
/// closed when the wrapper is disposed.
/// </summary>
/// <remarks>Use this to create a clone of the context web.</remarks>
/// <param name="web">The wrapper to clone.</param>
/// <returns>A new wrapper object.</returns>
public static WebWrapper CloneOf(WebWrapper web)
{
return CloneOf(web.Web);
}
/// <summary>
/// Sets the AllowUnsafeUpdates property to true on the
/// wrapped web object.
/// <remarks>
/// The setting is resat back in the dispose method, unless the
/// web itself is closed.
/// </remarks>
/// </summary>
public void AllowUnsafeUpdates()
{
Web.AllowUnsafeUpdates = true;
}
/// <summary>
/// Returns the url of the inner web.
/// </summary>
/// <returns>A value that equals <see cref="Web"/> <see cref="SPWeb.Url"/> property.</returns>
public override string ToString()
{
return webUrl;
}
/// <summary>
/// Returns a new <see cref="WebWrapper"/> object wrapping a new copy of the inner <see cref="Web"/> object.
/// The cloning is done using the <see cref="SPWeb.Url"/>, thus no security context is transferred to the new web.
/// </summary>
/// <remarks>The static method <see cref="CloneOf(SPWeb)"/> is used on the <see cref="Web"/> property.</remarks>
/// <returns>A new wrapper.</returns>
public WebWrapper Clone()
{
return CloneOf(Web);
}
/// <summary>
/// Implicitly wraps the web object in a <see cref="WebWrapper"/> object.
/// </summary>
/// <param name="web">The web to wrap.</param>
/// <returns>A new wrapper object. The original web may be accessed through the <see cref="Web"/> property.</returns>
public static implicit operator WebWrapper(SPWeb web)
{
return new WebWrapper(web, false);
}
/// <summary>
/// Explicitly extracts the <see cref="Web"/> value from the <see cref="wrapper"/>.
/// </summary>
/// <param name="wrapper">The object wrapping the <see cref="SPWeb"/> to extract.</param>
/// <returns>The inner <see cref="Web"/> of <see cref="wrapper"/>.</returns>
/// <remarks>The returned <see cref="SPWeb"/> object should be properly disposed after use.</remarks>
public static explicit operator SPWeb(WebWrapper wrapper)
{
wrapper.DoNotDisposeInnerWeb();
return wrapper.Web;
}
/// <summary>
/// Wrapper method for <see cref="SPWeb.GetList"/> on <see cref="Web"/> object.
/// </summary>
/// <param name="uri">A site relative uri to the list.</param>
/// <returns>A list if found.</returns>
public SPList GetList(Uri uri)
{
return web.GetList(uri.ToString());
}
/// <summary>
/// Wrapper method for <see cref="SPWeb.GetSiteData"/> on <see cref="Web"/> object.
/// </summary>
/// <returns>The results of the query,</returns>
public DataTable GetSiteData(SPSiteDataQuery query)
{
return Web.GetSiteData(query);
}
/// <summary>
/// Creates a new <see cref="SPWeb"/> as a sub web to this.
/// </summary>
/// <param name="url">The proposed local url of the new web. The nearest available is selected.</param>
/// <param name="name">The title of the new web.</param>
/// <param name="description">The description of the new web.</param>
/// <param name="language">The language of the new web. <remarks>If the language is not supported, the language of this is used.</remarks></param>
/// <param name="template">The site template to use.</param>
/// <returns>The new web wrapped in a new <see cref="WebWrapper"/> object.</returns>
[DebuggerStepThrough]
//debugger step through is to prevent this method to break when debugging, as it throws exceptions by [poor] design.
public WebWrapper CreateSubWeb(string url, string name, string description, uint language,
string template)
{
SPWeb newWeb;
try
{
newWeb = Web.Webs.Add(findSuitableWebUrl(url), name, description, language, template, true, false);
}
catch (SPException err)
{
if (err.ErrorCode == -2130575266)
{
//language not supported. Fallback to parent web language
newWeb = Web.Webs.Add(findSuitableWebUrl(url), name, description, Web.Language, template, true,
false);
}
else
throw;
}
return new WebWrapper(newWeb);
}
private string findSuitableWebUrl(string proposedName)
{
StringCollection names = new StringCollection();
names.AddRange(Web.Webs.Names);
int suffixIndex = 0;
const int maxIterations = 100000;
string name = proposedName;
while (names.Contains(name) && suffixIndex < maxIterations)
{
name = string.Format("{0}_{1}", proposedName, suffixIndex++);
}
return name;
}
/// <summary>
/// Calling this method will inhibit the default behaviour of closing the web on disposal.
/// </summary>
/// <remarks>Use with caution.</remarks>
internal void DoNotDisposeInnerWeb()
{
webShouldBeClosed = false;
}
}
}