【翻译】给Cookie穿上防弹衣

原文地址:Bullet Proof Cookies     演示代码:下载

简介

现在安全已经成为了一个热门话题;开发人员慢慢学会更多如何使自己的代码更安全和如何学习防御的编程技术。几年前使用防御编程是一种奢侈品,但现在再也不是了。随着在计算机世界里我们的威胁的增加,作为开发人员的我们应带有安全的意识在我们编程的时候。网络应用程序是应用程序的一种,在这我将重点讨论ASP.NET应用程序。然而,这些想法和概念这里讲到的也同样适合另外的网络编程语言。你们经常会看到Cookie在web应用程序的安全中扮演一个重要角色的文章。Cookie在web应用程序中有一些用处,例如ASP.NET本身使用Cookie来标识一个会话,一些网站用Cookie实现“记住我”的功能,另外一些网站用Cookie保存用户的偏爱设置。我将概要性的讲解Cookie和什么使它们容易受到攻击,我将给出一些例子演示Cookie是如何被滥用的最后我会谈谈我们需要做的,使我们的Cookie防弹战胜每一个漏洞。

代码展示

首先谈谈Cookie已有的一些问题然后给出每个问题的例子。Cookie有明文内容,用户可以查看到保存在自己电脑上的内容。有些Cookie保存用户的信息如用户名和密码,如果黑客对你的Cookie做了手脚那么你就有麻烦了,他会得到你的登录凭证劫持你的账号。黑客可以从网络嗅探您的Cookie,也可以窃取Cookie通过物理通道访问你的机器,或就在你的计算机上安装间谍软件。这些问题的答案已经是常识了,那就是对想保存的内容加密然后放在Cookie里。

Cookie的另一个问题是,您的Web应用程序是盲目的信任Cookie。我曾经在某网站上的有帐户,当我选择“保持登录状态”然后它给你一个保存了你账号ID的Cookie。这个账号ID是按照顺序排列的整数,我编辑了机器上的Cookie然后我重新打开那个网站,发现我已经登录到一个完全不同的账号,我可以操作这个Cookie使我登录那个系统的所有账户,这就被称作跳跃账户。还有更糟糕的,如果你登录到一个不同的账号,然后修改信息比如密码,密码是以明文发送给你的!为了解决这个问题,你们需要一种方法以确保这些Cookie问题上没有被改变或修改。有些人在这可能会问,如果加密这些Cookie不就解决问题了吗?一个人怎么能够操作加过密的Cookie?也许黑客总能发现你的 加密/解密 的密钥,通过仔细的你的加密模式,在此之上讨论超出了本文的范围,只要知道,这是一种可能黑客可以操作您的Cookie,然后,我们需要一个更好的解决方案,数字签名。

另一个关于cookies的问题是,当您发出一个cookie并给它一个截止日期,在你设置了时间之后你们完全信任浏览器发送给你的Cookie,也许不是这样的,Cookie在客户端可以被编辑在应用程序结束还有终止日期叶很容易改变。Firefox的一个不错附加组件--Cookie编辑器,它可以让你做这些事情。假设一个网站发给你一个唯一的标识符的cookie,让您保持登录状态,应用程序发出的cookie在一个星期后过期,以确保它不会永远保存,计算黑客不能实现账户跳跃,但如果标识符是绑定到一个用户帐户,然后,这个cookie可以用来永远登录到帐户通过操纵cookie的有效期。即使最终用户改变了他的密码,也不会有用的。为了解决这个问题,我们需要把绝对过期时间保存到cookie中,并通过阻止Cookie被操纵保证它不被更改。

Cookies也可用来绑定客户端服务器上的一个Session,由于HTTP无法识别连接的独特,它必须对每个请求发送会话cookie(同其他Cookies)。如果一个黑客得到您的Session cookie(通过嗅探在网络数据包或安装间谍软件),那么他将可以打开相同Session,您在您的浏览器中打开过的。此安全问题被称为会话劫持。一旦他劫持了你的回话,他可以改变您的密码和锁定你的账户,或者他能够获得敏感信息,如银行信息等。

请记住,即使是加密了cookie而且黑客无法读取其中的内容,他并不需要阅读其内容,窃取并在自己的机器发送它,这样就可以进入您的帐户。因此,我们还需要一种尽量独特方法来绑定Cookie和客户端让它变得更难破解而不是简单地把cookie放置到自己的浏览器,并获得他不应该有权限。

总括来说,我们需要通过加密确保Cookies里没有明。我们必须确保cookies不是操纵,它们实在是我们发出的数字签名。我们必须确保,当我们设计的Cookie必须包含一个确切的失效日期,我们应该能够相信这个过期时间,我们自己将过期日期假如到Cookie。 Cookie可以被黑客窃取并他手动的把Cookie放置在自己的电脑上,这样他就可以访问您的帐户,因此我们需要尝试绑定尽量独有的cookie到客户端的机器。

让我们多谈些关于绑定Cookie到客户端的机器的想法,这使得很难将Cookie移到另一台机器的浏览器上。最容易和简单的方法是让Cookie绑定到客户端的机器的IP地址。这不是一个完美的解决方案,因为它对于中间人攻击仍然是脆弱的,还有,如果黑客在同一用户的物理网络,他们使用一个IP地址连接到互联网(通过代理或NAT服务器),然后绑定到IP地址的做法还是不够的,因为两次个请求会被看做是来自同一个IP。我尝试过用另外一种更严格的绑定方法,我使用服务器变量“REMOTE_PORT”,它可以获取客户端的机器连接服务器的端口号,但这个方案并非完美,还因为它对于中间人攻击仍是脆弱的,但对黑客就更难处理这个cookie了。在使用“REMOTE_PORT”只有当服务器使用HTTP保持连接,并且至少有一个窗口打开网站,通常维持活动的标准时间为5分钟,因此在5分钟闲置后连接将被丢弃,当然这个参数是可以设置的。正如我所说,我尝试采用这个方法,我不能说这是一个完美的解决方案的原因很多,首先,如果用户想打开另一个窗口,新的窗口将迫使浏览器打开另一个连接,它会马上使您的程序丢失当前会话。用户是不会被迫保持连接的,这只是一个喜好,他可以关闭当前连接去打开另外一个窗口。如果这个用户是通过代理服务器的,那么代理服务器可能为了节省资源会关闭连接,这又会导致回话状态的丢失。根据不同的安全需要,您可能会考虑更严格的绑定“REMOTE_PORT”,如果我被要求写一个私人银行的应用程序,我会选择这种方法,安全利益的重要程度远远超过了方便不方便的问题了,然而假如你是在建立一个社区网站,我相信你的最终用户肯定会不满当他们打开另外一个窗口的时候突然抛出你的网站,或如果他使用代理服务器。你可选择任意多的变量绑定Cookie,如客户端使用代理,无论如何,我都想不到一个变量黑客不能复制的。尝试与其他服务器变量绑定到实验。

那么使用SSL和安全Cookie如何?SSL和安全Cookie能解决通过网络嗅探你Cookie的问题,然而使用SSL并不是让你Cookie更安全的解决方案。有办法使SSL容易受到中间人攻击人类,但它不是100%检测不到,当您认为cookie的是安全的,这并不意味着Cookie将在用户的机器加密,这是浏览器来决定如何保存它的,所以如果黑客有物理路径访问你的机器仍然可以利用这种Cookie的优势。设置SSL和标记为安全的始终是一个好主意,如果你能的话,它可以避免家庭网络的问题。最好方法是了解每个防守技术,它的作用,它解决了,然后你结合着使用这些方法以适应你的应用程序。

让我们开始干活吧。让我们从我们的cookie加密开始。 ASP.NET提供了一种非对称加密算法,非常时候我们需要的。我将使用RijndaelManaged 类加密我们的Cookie,因为它不仅功能强大且快速。我会写一个简单的函数,输入字符串,然后输入加密后的字符串。请记住,每一个应用程序开发应该有自己的钥匙。

private static byte[] key_192 = new byte[] 
{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
    10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10};

private static byte[] iv_128 = new byte[]
{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
    10, 10, 10, 10};

public static string EncryptRijndaelManaged(string value) {
    if (value == "")
        return "";

    RijndaelManaged crypto = new RijndaelManaged();
    MemoryStream ms = new MemoryStream();
    CryptoStream cs = new CryptoStream(ms, crypto.CreateEncryptor(key_192,
                         iv_128), CryptoStreamMode.Write);

    StreamWriter sw = new StreamWriter(cs);

    sw.Write(value);
    sw.Flush();
    cs.FlushFinalBlock();
    ms.Flush();

    return Convert.ToBase64String(ms.GetBuffer(), 0, (int)ms.Length);
}

同样,我们需要创建一个函数,将加密的字符串解密并返回明文。

public static string DecryptRijndaelManaged(string value) {
    if (value == "")
        return "";

    RijndaelManaged crypto = new RijndaelManaged();
    MemoryStream ms = new MemoryStream(Convert.FromBase64String(value));
    CryptoStream cs = new CryptoStream(ms, crypto.CreateDecryptor(key_192,
                        iv_128), CryptoStreamMode.Read);

    StreamReader sw = new StreamReader(cs);

    return sw.ReadToEnd();
}

上面的代码包括我们需要的加密,它是直截了当。您应该为每个不同的应用使用不同的密钥。下一步,我们将不得不写一个函数,对cookie的内容进行数字签名。为了对内容进行数字签名,我们需要使用非对称算法,当然我们将使用著名的RSA算法(公私密钥)。总之,为了给数据签名,我们需要有一特定的哈希散列算法的数据,然后用私钥对这个哈希散列签名,当我们想要验证签名后的数据,我们用公钥解密已加密的哈希散列然后和计算出来的哈希散列进行比较。我也希望能将几个变量保存到同一个cookie里,我将使用一个简单的XML结构使几个数据保存在一个cookie里,例如,在原来的Cookie里,截止日期是一块数据和其他可能的信息,如主机的IP地址。我会写一个函数,将需要提供以下几条信息,将它们合并(通过XML结构)签名,加密并返回的字符串作为一个单一的数据,以便把在一个cookie里并对称,我会写一个函数获取此字符串,解密,验证解密数据没有被更改过,最后分解成原来不同的数据。

public static string SignAndSecureData(string value)
{
    return SignAndSecureData(new string[] {value});
}

public static string SignAndSecureData(string[] values)
{
    string xmlKey = "MUST ADD YOUR OWN DEFAULT XML RSA KEY HERE";
    return SignAndSecureData(xmlKey, values);
}

public static string SignAndSecureData(string xmlKey, string[] values)
{
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.LoadXml("<x></x>");

    for (int i = 0; i < values.Length; i++)
        _AddNode(xmlDoc, "v" + i.ToString(), values[i]);

    RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
    rsa.FromXmlString(xmlKey);

    byte[] signature = rsa.SignData(Encoding.ASCII.GetBytes(xmlDoc.InnerXml),
        "SHA1");

    _AddNode(xmlDoc, "s", Convert.ToBase64String(signature, 0, 
                                                 signature.Length));
    return EncryptRijndaelManaged(xmlDoc.InnerXml);
}

下面面得代码主要是进行解密、验证、不同值的分离:

public static bool DecryptAndVerifyData(string input, out string[] values)
{
    string xmlKey = "MUST ADD YOUR OWN DEFAULT XML RSA KEY HERE";
    return DecryptAndVerifyData(xmlKey, input, out values);
}

public static bool DecryptAndVerifyData(string xmlKey, string input, 
                                        out string[] values)
{
    string xml = DecryptRijndaelManaged(input);

    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.LoadXml(xml);            

    values = null;

    XmlNode node = xmlDoc.GetElementsByTagName("s")[0];
    node.ParentNode.RemoveChild(node);

    // verify
    RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
    rsa.FromXmlString(xmlKey);

    byte[] signature = Convert.FromBase64String(node.InnerText);

    byte[] data = Encoding.ASCII.GetBytes(xmlDoc.InnerXml);
    if (!rsa.VerifyData(data, "SHA1", signature))
        return false;
    
    // count values
    int count;
    for (count = 0; count < 100; count++)
    {
        if (xmlDoc.GetElementsByTagName("v" + count.ToString())[0] == null)
            break;
    }

    values = new string[count];

    for (int i = 0; i < count; i++)
        values[i] = xmlDoc.GetElementsByTagName("v" + 
                                                i.ToString())[0].InnerText;

    return true;
}

请注意,为了获得一个XML RSA密钥,只需创建一个RSACryptoServiceProvider对象,并调用ToXmlString方法获取密钥(包括签名私钥)。
请注意,在这个阶段,你有两个非常强大的方法SignAndSecureData,DecryptAndVerifyData,它们可以在您的代码在许多地方用于加密和签名的数据。我现在写了两个方法,将会利用这两种方法对Cookie进行签名和加密还有对称解密和验证的Cookie。

public static void SignAndSecureCookie(HttpCookie cookie,
                               NameValueCollection serverVariables) {
    if (cookie.HasKeys)
        throw (new Exception("Does not support cookies with sub keys"));

    if (cookie.Expires != DateTime.MinValue) // has an expiry date
    {
        cookie.Value = SignAndSecureData(new string[] 
    {cookie.Value, 
        serverVariables["REMOTE_ADDR"], 
        cookie.Expires.ToString()});
    }
    else
    {
        cookie.Value = SignAndSecureData(new string[] { cookie.Value, serverVariables["REMOTE_ADDR"] });
    }
}

public static string DecryptAndVerifyCookie(HttpCookie cookie,
                                       NameValueCollection serverVariables) {
    if (cookie == null)
        return null;

    string[] values;

    if (!DecryptAndVerifyData(cookie.Value, out values))
        return null;

    if (values.Length == 3) // 3 values, has an expiry date
    {
        DateTime expireDate = DateTime.Parse(values[2]);
        if (expireDate < DateTime.Now)
            return null;
    }

    if (values[1] != serverVariables["REMOTE_ADDR"])
        return null;

    return values[0];
}

在这一点上,我们现在手头的代码来执行所有的任务,我们要保护我们的cookies。
让我们看看如何使用此代码做两共同的任务。第一项任务是,为了更好的保护会话cookie。我们将要做的是重写/继承 Global.aspx文件三个方法。第一个方法是在session_start将用来创建一个Cookie,我们将用来保存会话ID,但我们的Cookie特殊因为它是经过签名的,以防止它被修改,而且也绑定了用户的IP地址。这将使劫持会话变得非常困难,如果你想更严格的约束,您可能需要考虑我在这篇文章上述的想法了,使cookie绑定REMOTE_PORT也。第二个方法,我们将执行PreRequestHandlerExecute事件处理程序。在此方法中,我们将检查会话cookie的ID和保存在签名后的Cookie,如果他们不匹配,那么我们就会放弃会话。最后,我们将执行一个方法Session_End,以帮助保证,浏览器会删除我们签署的cookie。下面是所有这些代码,注意,该代码在Global.aspx文件去,两个功能已经存在和一个事件必须添加的事件处理函数。

protected void Session_Start(Object sender, EventArgs e) {
    System.Web.HttpCookie cookie
                     = new System.Web.HttpCookie("__signed_session",
  Session.SessionID);
    CHelperMethods.SignAndSecureCookie(cookie, Request.ServerVariables);
    Response.Cookies.Add(cookie);
}

private void Global_PreRequestHandlerExecute(object sender,
                                             System.EventArgs e) {
    if (CHelperMethods.DecryptAndVerifyCookie(
                            Request.Cookies["__signed_session"],
                            Request.ServerVariables) != Session.SessionID)
    {
        Session.Abandon();
        Response.Redirect("http:// YOUR MAIN PAGE HERE");
    }
}

protected void Session_End(Object sender, EventArgs e) {
    Response.Cookies["__signed_session"].Expires = DateTime.Now.AddDays(-1);
}

第二个共同的任务,我想告诉你是如何添加代码能更简单的使用安全Cookie。在我的程序中,我添加了一个通用类,我把它用作所有页面的基类,我建议你们也一样,因为这将被证明很有用,例如在同一个基类,我用一个方法将混淆电子邮件地址通过注入JavaScript代码。这将防止您的网站的电子邮件地址被蜘蛛得到。你可以查看附加压缩文件中的代码。您也可以下载一个免费的工具,我的网站有款名为“Coder's TextObfuscator”的软件,可以完成这个。
我将创建两个保护方法,你将能够使用所有的网页; RequestSecureCookies和ResponseSecureCookies。一个方法仅仅让你添加,另一个方法是如果Cookie有效就获取cookie(从请求)。你可以查看附加压缩文件中的代码。

posted @ 2009-12-27 20:28  吕飞  阅读(1239)  评论(4编辑  收藏  举报