65.9K
CodeProject 正在变化。 阅读更多。
Home

URL 请求伪造的预防方法——以 ASP.NET MVC 为例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (13投票s)

2010年11月25日

CPOL

7分钟阅读

viewsIcon

78201

downloadIcon

1742

这是一种新颖的方法,可以防止通过 URL 字符串传递的参数被篡改。

引言

没有不考虑安全问题而开发的Web应用程序。但安全措施是否正确实施以及应用程序是否受到良好保护,这一点并不确定。我们可以看到许多Web应用程序的查询字符串参数是明文的,这很容易被操纵,并且可能导致受限资源的升级。即使URL参数经过加密,仍然存在通过操纵加密哈希进行暴力攻击的机会,当数据库很大时,匹配随机哈希的机会很高,这种攻击就会奏效。因此,很明显,保护Web应用程序中的URL对于所有编程平台都是必要的。在此,将结合 ASP.NET MVC 讨论此问题,并提供实用程序类。

背景

.NET Framework 提供了许多改进的安全方法,MVC 框架引入了新的安全方法,例如 AntiForgeryToken。让我们简要了解一下常见的安全方法。

  • Authorize 属性:这在用户或角色经过身份验证后,用于授权其访问应用程序中的任何资源。这有助于实现用户和角色级别的保护。
  • HTTP Referrer 检查:这是一种通用方法,不限于 .NET。它用于防止来自站外链接或直接在浏览器导航栏中执行的链接发起的 URL 请求。
  • 防伪令牌:这是一个强大的选项,可以防止表单提交时对隐藏字段的操纵,并防止跨站请求伪造。
  • HTML编码:建议对所有用户输入进行编码,以防止跨站脚本攻击/XSS攻击等。
  • 防止 SQL 注入
  • 查询字符串参数的加密。这是一种防止查询字符串参数被篡改的好方法。但这并不是完全的保护,仍然容易受到暴力攻击。

尽管上述所有方法都可用,但 .NET 框架没有提供用于保护 URL 参数的内置功能。其 URL 授权技术保护整个 URL,但不会保护 URL 中仍然容易受到操纵的部分。这里的这段代码有助于轻松实现所有这些安全方法,特别注重保护 URL 参数。创建 URL 哈希并不是新事物,但在行业中仍不是一种常见做法,尽管它是一个重要的关注点。因此,分享一段代码以便在 ASP.NET MVC 中轻松实现此方法将非常重要。

Using the Code

基本思想很简单,将 URL 参数以及控制器、操作名称加密成一个哈希值,然后将其作为 URL 参数传递给服务器。服务器端将通过重新计算哈希值并与提交的哈希值进行比较来验证其有效性。此哈希值是动态的且特定于会话。因此,即使您的 URL 在中间人攻击中被缓存,它也不会在其他会话中起作用。基本上,一个实用程序类扩展了 HTML 助手 ActionLink 类。因此,我们可以像在 MVC 实现中一样使用 Html.ActionLink 方法,此外,您只需将一个布尔参数作为最后一个参数传递即可获得受保护的 URL。使用此代码非常简单。示例如下所示。

(附带源代码:使用 Visual Studio Express 2010、SQL Express 开发。此开发平台可免费获取。)

尝试演示项目,或将演示项目中的 ActionLinkExtensions.cs 文件包含到您的项目中。

该图显示了包含核心代码的文件,这些文件可以在您的项目中重用。

在您的视图中,页面声明下方添加以下行:

注意
  • 用红色表示的布尔参数是获取受保护 URL 的唯一附加参数,除了 ASP MVC HTML 中 ActionLink 方法的 10 个重载参数之外。
  • ExtendedHtmlActionLinkHelper - 它是此示例项目的命名空间,在您的实现中会更改。
  • RouteValues()HtmlAttributes() 是测试类,分别包含超链接的测试参数和测试属性。在您的实现中会更改。

控制器代码

在视图中获取受保护的 URL 就像传递一个布尔参数一样简单。现在让我们看看提交到控制器时如何验证 URL。这甚至更简单。只需将此属性添加到控制器方法中即可。

[IsValidURLRequest]

此属性负责简单的授权检查(用户名、密码验证)、HTTP Referrer 检查以及保护 URL 参数。
然而,还有一件事需要处理路由,以避免与路由混淆。在 global.asax 中的路由中将“urltoken”添加为第一个参数,并且所有控制器方法都将其作为第一个参数。这可以是一个可选参数。路由映射示例如下。

routes.MapRoute(
      "Default", // Route name
      "{controller}/{action}/{urltoken}/{id}", // URL with parameters
      new { controller = "Home", action = "Index", urltoken = UrlParameter.Optional, 
	id = UrlParameter.Optional } // Parameter defaults
);

模型

此模型代码在您的项目中是完全可重用的。但是,它依赖于名为 ErrorviewController 的控制器以及两个操作方法 DisplayURLErrorDisplayHttpReferrerError,这些方法需要在您的项目命名空间中可用。

所有用于生成和验证安全 URL 的模型代码都在演示项目中的 ActionLinkExtensions.cs 文件中,命名空间为 SecuredUrl.Links。该文件可以包含在任何项目中并重复使用。基本上,它扩展了 HTML 帮助器方法“ActionLink”。关于如何扩展 HTML 帮助器方法,CodeProject 中有大量信息,无需过多讨论。让我们讨论 URL 哈希是如何形成的。请看以下代码:

//This is a static helper class which creates the URL Hash
public static class TokenUtility
{
       //This method creates the URL hash and attach it to the URL.
 public static string generateUrlToken(string controllerName, string actionName, 
 	RouteValueDictionary argumentParams, string password)
    {
        //The URL hash is dynamic by assign a dynamic key in each session. So
        //even though your URL is stolen, it will not work in other session
        if (HttpContext.Current.Session["url_dynamickey"] == null)
        {
            HttpContext.Current.Session["url_dynamickey"] = RandomString();
        }
        string token = "";
        //The salt include the dynamic session key and valid for an hour.
        string salt = HttpContext.Current.Session["url_dynamickey"].ToString() + 
        DateTime.Now.ToShortDateString() + " " + DateTime.Now.Hour; ;
        //generating the partial url
        string stringToToken = controllerName + "/" + actionName + "/";
        foreach (KeyValuePair<string, object> item in argumentParams)
        {
            if (item.Key != "controller" && 
            item.Key != "action" && item.Key != "urltoken")
            {
                stringToToken += "/" + item.Value;
            }
        }
        //Converting the salt in to a byte array
        byte[] saltValueBytes = System.Text.Encoding.ASCII.GetBytes(salt);
        //Encrypt the salt bytes with the password
        Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, saltValueBytes);
        //get the key bytes from the above process
        byte[] secretKey = key.GetBytes(16);
        //generate the hash
        HMACSHA1 tokenHash = new HMACSHA1(secretKey);
        tokenHash.ComputeHash(System.Text.Encoding.ASCII.GetBytes(stringToToken));
        //convert the hash to a base64string
        token = Convert.ToBase64String(tokenHash.Hash).Replace("/","_");
        return token;
    }
    //This method validates the token
    public static bool validateToken(string token,string controllerName, 
    string actionName, RouteValueDictionary argumentParams, string password)
    {
        //compute the token for the current parameter
        string computedToken = generateUrlToken
        (controllerName, actionName, argumentParams, password);
        //compare with the submitted token
        if (computedToken != token)
        {
            computedToken = generateUrlToken
            ("", actionName, argumentParams, password);
        }
        else { return true; }
        
        if (computedToken != token)
        {
            return false;
        }
        else { return true; }
    }
    //It validates the token, where all the parameters passed as a RouteValueDictionary
    public static bool validateToken
	(RouteValueDictionary requestUrlParts, string password)
    {
        //get the parameters
        string controllerName;
        try
        {
            controllerName = Convert.ToString(requestUrlParts["controller"]);
        }
        catch (Exception e)
        {
            controllerName = "";
        }
        string actionName = Convert.ToString(requestUrlParts["action"]);
        string token = Convert.ToString(requestUrlParts["urltoken"]);
        //Compute a new hash
        string computedToken = generateUrlToken
        (controllerName, actionName, requestUrlParts, password);
        //compare with the submitted hash
        if (computedToken != token)
        {
            computedToken = generateUrlToken
            ("", actionName, requestUrlParts, password);
        }
        else { return true; }
        
        if (computedToken != token)
        {
            return false;
        }
        else { return true; }
    }
    //This method create the random dynamic key for the session
    private static string RandomString()
    {
        StringBuilder builder = new StringBuilder();
        Random random = new Random();
        char ch;
        for (int i = 0; i < 8; i++)
        {
            ch = Convert.ToChar(Convert.ToInt32
		(Math.Floor(26 * random.NextDouble() + 65)));
            builder.Append(ch);
        }
        return builder.ToString();
    }
}

//This is a attribute class which actually calls 
//the validation and to be used with the controller
public  class IsValidURLRequestAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
       // basic authorization check 
        base.OnAuthorization(filterContext);
        if (filterContext.HttpContext != null)
        {
            //Http referrer check and do the redirection if error occurs
            //It uses a controller named ErrorViewController and 
            //action named DisplayHttpReferrerError
            //These controller and action need to be present 
            //in your project in the project name space
            if (filterContext.HttpContext.Request.UrlReferrer == null)
            {
                filterContext.Result = new RedirectToRouteResult(
                     new System.Web.Routing.RouteValueDictionary
                    {
                            { "langCode", 
                            filterContext.RouteData.Values[ "langCode" ] },
                            { "controller", "ErrorView" },
                            { "action", "DisplayHttpReferrerError" },
                            { "ReturnUrl", filterContext.HttpContext.Request.RawUrl },
                    });
            }                    
        }
        
        /*Add code here to check the domain name the request come from*/
        
        // The call for validation of URL hash and do the redirection if error occurs
        //It uses a controller named ErrorViewController and action named DisplayURLError
        //These controller and action need to be present in your project 
        //in the project name space
        if (TokenUtility.validateToken
        (filterContext.RequestContext.RouteData.Values, 
		ActionLinkExtensions.tokenPassword) == false)
        {
            filterContext.Result = new RedirectToRouteResult(
                 new System.Web.Routing.RouteValueDictionary
                    {
                            { "langCode", filterContext.RouteData.Values[ "langCode" ] },
                            { "controller", "ErrorView" },
                            { "action", "DisplayURLError" },
                            { "ReturnUrl", filterContext.HttpContext.Request.RawUrl }
                    });
        }
    }
}

上述代码不言自明。它使用会话相关的密钥创建哈希并将其附加到 URL。有一个验证属性,用于验证基本授权、HTTPReferrer 和 URL 哈希。

概念验证

运行演示应用程序。点击页面中的任何链接。如果未登录,则会重定向到登录页面。进行下一个测试,登录(自行注册或使用此登录用户:albin;密码:test123),然后将链接复制到浏览器新选项卡的导航栏中,这是操纵参数(伪造)并执行链接的一种方式。它不会通过 httpreferrer 检查。正如我在代码中提示要检查外部链接,请添加代码以从 HTTP referrer 检查域。现在注入此 JavaScript。

(登录后,将以下 JavaScript 粘贴到导航栏中,然后按回车键。它将创建一个“测试我”链接。)

javascript:alert(document.getElementById("testplace").innerHTML="<a href=
	'<right click the link in the demo page-> copy link location than paste here. 
	Manipulate the parameter '>Test Me</a>");

将 JavaScript 中的 URL 替换为从演示页面复制的 URL,然后操纵参数,然后按上述方式执行。

例如,点击“链接测试 2”链接。它打开下一页并显示结果

右键单击链接并复制链接位置

更改参数。

javascript:alert(document.getElementById("testplace").innerHTML=
"<a href='https://:59248/Home/handleLink/fjr2orS7nxTDjyetxJRb%2brJOd_k%3d/200>
Test Me</a>");

现在将其粘贴到导航栏中并回车。在警报消息上单击“确定”。您将得到一个“测试我”链接,如下所示:

现在点击“测试我”链接。结果将是

这通过了 httpreferrer 检查,但由于 URL 参数被篡改,在 URL 哈希检查中失败。如果未进行此检查,黑客可能会访问其他用户的数据。此 URL 哈希是会话特定的。关闭浏览器并重新运行演示。您会发现哈希值已更改。

总而言之,这种 URL 哈希方法为防止 URL 参数伪造提供了良好的安全性。如果您真的想让黑客的生活变得艰难,请结合使用加密参数和这种 URL 哈希方法。

确保安全!

关注点

我写这篇小文章是为了让这种方法在常见的 Web 应用程序中得到应用,因为我遇到许多网站在 URL 中不安全地传递参数。所以我想到让这种方法更常见,我选择了流行的 CodeProject 来发布它。最后,如果存在错误,请原谅我。在我的下一篇文章中,我将写关于如何在 Ajax 请求中实现这种方法。

© . All rights reserved.