防弹 Cookie






4.37/5 (23投票s)
2007 年 2 月 28 日
14分钟阅读

105802

565
一篇关于以多种方式保护您的 Cookie 以克服 Cookie 的各种漏洞的文章。
- 下载源代码 - 11.4 KB (运行前必须设置RSA密钥)
引言
安全性是当今的热门话题;开发人员正逐渐了解如何使代码更安全以及如何学习防御性编程技术。多年前,防御性安全编程还是一种奢侈品,但现在已不再是。随着我们身处的计算机世界威胁的增加,我们作为开发人员,在编写代码时应牢记安全概念。一类应用程序是 Web 应用程序,特别是在本文中,我将讨论 ASP.NET 应用程序。但是,这里的想法和概念可以应用于任何 Web 编程语言。您总是会读到 Cookie 在 Web 应用程序的安全性中扮演着重要角色。Cookie 在 Web 应用程序中有多种用途,例如 ASP.NET 本身使用 Cookie 来识别会话,一些网站使用 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,我们需要更好的解决方案,即数字签名。
Cookie 的另一个问题是,当您颁发一个 Cookie 并为其设置过期日期时,您相信浏览器会在设定的日期之后停止向您发送 Cookie,不仅如此,Cookie 可以在应用程序的客户端被编辑,其过期日期很容易被更改。FireFox 的一个不错的附加组件是 Cookie 编辑器,它允许您执行所有这些操作。假设一个网站向您发送一个具有唯一标识符的 Cookie 以保持您的登录状态,该应用程序颁发该 Cookie 的有效期为 1 周,以确保它不会永远保存,即使攻击者无法在 Cookie 上进行账户跳转,但如果标识符绑定到用户账户,那么这个 Cookie 可以通过操纵 Cookie 的过期日期来永久登录账户。即使最终用户更改了密码,也无济于事。要解决这个问题,我们需要在 Cookie 内容本身中放入我们自己的绝对过期日期,并通过阻止 Cookie 被操纵来保护它免受更改。
Cookie 也用于将客户端绑定到服务器上的会话,由于 http 没有唯一识别连接的方法,因此它必须在每次请求中发送会话 Cookie(以及其他 Cookie)。如果攻击者获取了您的会话 Cookie(通过网络嗅探数据包或安装间谍软件),那么他将能够打开您在浏览器中打开的同一会话。这种安全问题被称为会话劫持。一旦他劫持了您的会话,他就可以更改您的密码并将您锁定在您的帐户之外,或者他可以访问敏感信息,如银行信息。
请记住,即使 Cookie 被加密了,攻击者也无法读取其内容,他不必读取其内容就可以窃取它并从自己的机器上发送,从而可能访问您的帐户。因此,我们还需要一种方法来尽可能唯一地将 Cookie 绑定到客户端的机器上,以使攻击者更难将其 Cookie 放到自己的浏览器中并获得他不应有的访问权限。
总而言之,我们需要确保 Cookie 不以纯文本形式保存,通过加密它们。我们需要确保 Cookie 未被篡改,并且它们确实是由我们颁发的,通过对其进行数字签名。我们需要确保当我们在设计 Cookie 时考虑到特定的过期日期时,我们应该能够信任该过期日期,我们通过将日期添加到 Cookie 内容本身来实现这一点。Cookie 可能被攻击者窃取并手动放入他的机器,以便他可以访问您的帐户,因此我们需要尝试尽可能唯一地将 Cookie 绑定到客户端的机器上。
让我们更深入地探讨一下将 Cookie 绑定到客户端机器以防止其被盗用到另一台机器上的浏览器上的想法。最简单直接的方法是将 Cookie 绑定到客户端机器的 IP 地址。这不是一个完美的解决方案,因为它仍然容易受到中间人攻击,而且如果攻击者与用户在同一个物理网络上,并且他们使用一个 IP 地址连接到互联网(通过代理或 NAT 服务器),那么绑定到 IP 地址将不够,因为两次请求似乎都来自同一个 IP。我尝试了另一种方法,它使绑定非常紧密,我使用了服务器变量“REMOTE_PORT
”,它提供了连接建立的客户端机器的端口号,然而这个解决方案也不完美,因为它仍然容易受到中间人攻击,但攻击者很难获取这个 Cookie。只有当服务器使用 http keep alive 并且浏览器有一个打开的指向该网站的窗口时,才可能使用“REMOTE_PORT
”,并且标准的 keep alive 时间是 5 分钟,因此连接将在 5 分钟不活动后断开,当然可以更改此参数。正如我所说,我尝试了这种方法,我不能说它是完美的解决方案,原因有很多,首先,如果用户想打开另一个窗口,新窗口将强制浏览器打开另一个连接,这实际上将导致您的应用程序放弃会话。浏览器不被强制保持连接活动,这只是一个偏好,浏览器可以关闭连接并打开另一个。如果最终用户使用代理服务器,代理服务器可能会选择关闭连接以节省资源,因此这同样会有效地断开会话。根据您所需的安全性强度,您可能需要考虑绑定到“REMOTE_PORT
”,如果有人让我编写一个私人银行应用程序,我会选择这种方法,安全优势大于不便之处,但是如果您正在创建一个社区网站,例如,我敢肯定,当您的最终用户打开另一个窗口或他们位于代理服务器后面时,被踢出网站会让他们感到恼火。您可以选择任意数量的服务器变量来绑定,例如客户端的用户代理,但我想不到一个攻击者无法复制的变量。尝试试验其他服务器变量进行绑定。
SSL 和安全 Cookie 怎么样?SSL 和安全 Cookie 可以解决在网络上嗅探您的 Cookie 的问题,然而 SSL 并不是保护您的 Cookie 的完美解决方案。有方法可以使 SSL 容易受到中间人攻击,尽管它不是 100% 不可检测的,而且当您将 Cookie 标记为安全时,这并不意味着 Cookie 会在最终用户的机器上被加密,这取决于浏览器如何存储它,因此,具有物理访问您机器的攻击者仍然可以利用这样的 Cookie。如果可能,设置 SSL 并将您的 Cookie 标记为安全总是一个好主意,它可以保护您免受一系列问题的困扰。最好的编码方法是理解每种防御技术,它的作用,它解决了什么问题,然后根据您应用程序的需要组合使用这些方法。
让我们动手实践。让我们从加密我们的 Cookie 开始。ASP.NET 提供了一系列对称加密算法来满足我们的大部分需求。我将使用 RijndaelManaged 类来加密我们的 Cookie,因为它既强大又快速。我将编写一个简单的函数,它接受字符串输入并返回相同的加密数据字符串。请记住,您开发的每个应用程序都应该有自己的密钥。
// must create your own keys and ivs
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 密钥,您可以简单地创建一个 RSACryptoServicePr
ovider 对象并调用 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];
}
至此,我们现在已经掌握了实现我们想要保护 Cookie 的所有任务的代码。
让我们看看如何使用这些代码来完成两个常见的任务。第一个任务是更好地保护我们的会话 Cookie。我们将通过覆盖/实现 Global.aspx 文件中的三个函数来做到这一点。第一个函数是 Session_Start
,它将用于创建我们自己的另一个 Cookie,其中包含会话 ID,但是,我们的 Cookie 是特殊的,因为它将被签名,以防止被修改,并且它将绑定到最终用户的 IP 地址。这将有助于使您的会话极难被劫持。如果您想使绑定更紧密,您可能需要考虑本文前面给出的想法,也将 Cookie 绑定到 REMOTE_PORT
。我们将实现的第二个函数是 PreRequestHandlerExecute
的事件处理程序。在此函数中,我们将将会话 Cookie ID 与我们存储在签名 Cookie 中的 ID 进行比较,如果它们不匹配,我们将放弃会话。最后,我们将实现另一个函数 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);
}
我想向您展示的第二个常见任务是如何在您的 Web 应用程序中轻松添加代码以启用安全 Cookie 的使用。在我的 Web 应用程序中,我有一个通用类,用作我所有页面的基类,我建议您也这样做,因为它将来证明有用,例如,在我使用的这个基类中,我有函数可以混淆电子邮件地址,通过注入 JavaScript 代码来隐藏电子邮件地址。这将阻止爬虫从您的网站上抓取电子邮件。您可以在附加的 zip 文件中查看代码。您还可以下载我在我的网站上提供的免费工具“Coder's TextObfuscator”,它可以在多种编程语言中执行此操作以及更多功能。
我将创建两个受保护的函数,您可以在所有页面中使用它们;RequestSecureCookies
和 ResponseSecureCookies
。一个函数将简单地允许您将 Cookie 添加到您的响应中,另一个函数将在 Cookie 有效时为您返回 Cookie(从请求中)。您可以在附加的 zip 文件中查看代码。
几句警告...
与编程中的其他任何事物一样,存在权衡。在您继续将此代码用于所有应用程序之前,请记住,加密/签名/解密/验证代码会带来额外的处理开销。明智地使用安全 Cookie,仅将其添加到敏感 Cookie 中,不要过度使用。请记住,Cookie 在每次请求时都会发送,这意味着每次请求都会有额外的解密/验证处理。加密 Cookie 的大小也会略有增加。
结论
我希望我的想法和代码能帮助您编写更好、更安全的代码,请随时向我提问。请明智地使用代码,并记住没有完美的解决方案,但您可以让那些坏人难很多。我将花点时间提醒您客户端证书,这是我从未见过任何网站使用的功能。如果您需要最高级别的安全性,除了使用本文提出的想法之外,您可能还应该考虑实现客户端证书。祝您编码安全...