AllowUnsafeUpdates的最佳模式

时间:2008-10-17 21:14:33

标签: sharepoint spweb spsite

到目前为止,在我的研究中,我发现在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

        }
    }

赞赏对最佳模式的反馈。

6 个答案:

答案 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;
        }

    }
}