为什么将会话 ID 直接存储在 cookie 中是不安全的?

信息安全 Web应用程序 饼干 会话管理
2021-09-06 13:57:28

我正在学习会话中间件

必须提供一个secret或中间件抱怨:

app.use(session({
  secret: "abc",
  resave: false,
  saveUninitialized: false,
  store: new MongoStore({
    mongooseConnection: mongoose.connection
  })
}));

我做了一些调查,实际的会话 ID 是

eKeYlF1DR6AtVkeFZK9vEIHSZT8e0jqZ

但是根据cookie,会话ID是

s%3AeKeYlF1DR6AtVkeFZK9vEIHSZT8e0jqZ.on5ifVE079C4ctKNdkNiJSh8NkQMckjd5fn%2FsxIQWCk

我很迷惑。为什么将会话 ID 直接存储在 cookie 中是不安全的?我不能直接存储会话 ID 吗?

看起来这secret是一个私钥并且会话 ID 正在被加密?

据我了解,如果攻击者可以通过 XSS 或社会工程获取此 cookie,则攻击者仍然可以劫持会话。我不确定这是什么意思secret

3个回答

那个 JS 库的作者似乎做了一个常见但错误的假设,尽管基于足够的知识来弄错。

你不能只是撒上魔法加密仙尘,然后期望获得更多的安全性,比如巧克力片。

作者缺少的是,一旦您签署了会话 ID,并将其放入 cookie - 签名的会话 ID 就是会话 ID。实际上是最终用户用来告诉服务器哪个会话是他们的会话的标识符。服务器是否愿意进行一些内部处理以将实际会话标识符与会话内存的内部表示相关联并不重要。

需要明确的是,如果签名的会话 id 被盗 - 以任何方式,无论是通过 XSS、社会工程或其他任何方式 - 那么攻击者将能够简单地使用该签名的会话 id 来劫持用户的会话,尽管内部表示。


也就是说,在将 cookie 值发送到浏览器之前对其进行签名有一个名义上的好处:篡改检测。也就是说,在接收到用户的 cookie 后,Web 应用程序可以在使用它来查找会话内存之前验证该 cookie 没有被篡改,即通过验证签名。这可能会通过避免对从未有效的会话 id 进行会话查找来防止对会话管理的某些高级/主动攻击。

然而,这种假设的好处是值得怀疑的,因为会话 id 通常只是一个 id,用于查找会话变量。此外,大多数针对会话 id 的攻击都不会因此而停止 - 会话劫持、会话固定、会话捐赠、会话骑行等......

无论如何,如果你真的想在使用之前验证会话 cookie 没有被篡改,有比数字签名更简单的方法......

来自https://web.archive.org/web/20160306144238/http://hueniverse.com/2015/07/08/on-securing-web-session-ids/的更完整答案

免责声明:就像不了解您自己系统细节的人提供的任何安全建议一样,这仅用于教育目的。安全是一个复杂且非常具体的领域,如果您担心系统的安全性,您应该聘请一位专家来审查您的系统以及威胁分析并提供适当的建议。

蛮力

蛮力攻击是攻击者试图通过使用不同的凭据发出重复请求(直到一个有效)来访问系统的攻击。最常见的示例是尝试猜测用户密码的攻击者。这就是为什么密码应该很长并避免使用字典单词以使其难以猜测的原因。设计合理的系统会跟踪失败的身份验证请求,并在出现攻击时升级问题。

密码不是 Web 身份验证中使用的唯一凭据。最常见的实现包括一个登录页面,该页面在成功验证后会在客户端上设置一个会话 cookie。会话 cookie 充当不记名令牌 - 使用令牌出现的人被视为经过身份验证的用户。设置会话 cookie 无需在每个页面上输入您的用户名和密码。但是,此会话 cookie 现在充当唯一的身份验证密钥,任何获得此密钥访问权限的人都将获得对系统的访问权限。毕竟,Cookie 只是一个简单的字符串。

会话 id 猜测攻击是一种蛮力攻击。攻击者不是试图猜测密码,而是试图猜测会话 id 并伪造身份验证 cookie。攻击者生成会话 ID 并尝试使用这些 ID 发出请求,希望它们与实际活动会话匹配。例如,如果 Web 应用程序会话 ID 是按顺序生成的,则攻击者可以查找他们自己的会话 ID,并根据该伪造请求使用附近的会话 ID 值。为了防止这种攻击,我们需要让猜测会话 ID 变得不切实际。请注意,我说的是“不切实际”,而不是“不可能”。

不切实际

第一步是确保会话 ID 足够长且不连续。就像密码一样,会话 id 越长,就越难通过猜测找到有效的。同样重要的是,会话 ID 不是使用可预测的算法(例如计数器)生成的,因为如果存在这种逻辑,攻击者将不再猜测而是生成会话 ID。使用加密安全的随机数生成器来生成足够长的会话 ID 是最好的常见做法。什么是“足够长”?好吧,这取决于您的系统的性质。大小必须转化为猜测有效会话 ID 的不切实际的努力。

防止攻击者猜测会话 ID 的另一种方法是通过向会话 cookie 添加哈希或签名来将完整性构建到令牌中。Express 会话中间件执行此操作的方式是计算会话 id 和密钥组合的哈希值。由于计算哈希需要拥有秘密,因此攻击者将无法在不猜测秘密(或只是试图猜测哈希)的情况下生成有效的会话 ID。就像强随机会话 id 一样,哈希大小必须与它要保护的特定应用程序的安全要求相匹配。这是因为最后,会话 cookie 仍然只是一个字符串,并且容易受到猜测攻击。

会话 ID 必须足够长且无法猜测。有几种方法可以做到这一点。上面的随机性和散列技术是两种最常见的方法,但不是唯一的。

图层

如果我们生成强随机会话 ID,我们还需要哈希吗?绝对地!

核心安全主体是分层。这也被称为不把所有的鸡蛋放在一个篮子里。如果您依赖单一安全来源,那么如果单一来源出现故障,您最终将完全没有安全性。例如,如果有人在您的随机数生成器中发现错误怎么办?如果他们找到一种方法来破解您系统的那部分并替换它怎么办?有无数已知的攻击正是利用这一点 - 随机数的生成毕竟不是那么随机的。

将强随机会话 id 与完整性哈希相结合将防止随机数生成器中的缺陷。它还将防止开发人员错误,例如使用错误的随机数生成器函数(例如,每个系统都提供的非随机方法以及强方法)。无论我们的流程多么出色或经验多么丰富,我们都会编写糟糕的代码。它是软件工程的一部分。这就是为什么分层安全性如此重要的原因。护城河是不够的,你还需要在它后面有一堵墙,可能还有一些守卫在墙后面。

如果您认为在 OpenSSL 中使用错误的随机函数或深度错误是这里仅有的两个问题,请考虑在 JavaScript 和其他动态语言中猴子修补代码的常见做法。如果整个应用程序部署中的任何地方有人扰乱了全局随机设施(用于测试、日志记录等)并破坏了它(或者它是恶意代码注入的一部分),那么仅依赖随机性的会话 ID 不再安全。

警报

猜测密码和猜测会话ID 之间的一个重要区别是密码与帐户(例如用户名)相关联。帐户密码对更容易跟踪暴力攻击,因为它提供了一种相对简单的方法来跟踪失败的尝试。然而,当涉及到会话 ID 时,它就没有那么简单了,因为会话过期并且不包含帐户上下文。这意味着无效的会话 id 可能来自过期会话或攻击者,但如果没有额外的数据(例如 IP 地址),在大型系统中将很难区分。

通过在会话 id 中包含完整性组件(通过散列或签名),服务器可以立即区分过期会话、未分配会话 id 和无效会话。即使您只是记录无效的身份验证尝试(并且您应该),您也希望以不同于记录无效会话的方式记录过期会话。除了了解差异的安全价值之外,它还将提供有关用户行为方式的有用见解。

卫生

凭证应该过期,因此会话 ID 应该有一个有限的生命周期(其中持续时间是一个非常特定于系统的值)。虽然 cookie 带有过期策略,但无法确保它确实得到遵守。攻击者可以将 cookie 过期设置为任何值,而服务器无法检测到它。一个常见的最佳实践是在每个颁发的凭证中包含一个时间戳,这可以像在随机生成的会话 id 中添加一个时间戳后缀一样简单。然而,为了依赖这个时间戳,我们必须能够验证它没有被篡改,而实现这一点的方法是使用哈希或签名。

将时间戳添加到会话 id 允许服务器快速处理过期会话,而无需进行昂贵的数据库查找。虽然这听起来可能与安全无关,但它实际上是维护安全应用程序的核心。

拒绝服务攻击(或 DoS)是一种攻击,其中攻击者发出重复请求,其唯一目的是消耗服务器上的过多资源,然后将其关闭或使其他人无法访问。如果每个请求身份验证都需要在应用层进行完整的数据库查找,则攻击者可以使用伪造的会话 ID 轻松发起 DoS 攻击。通过在 cookie 中包含完整性组件,服务器可以立即识别伪造或过期的凭据,而无需任何后端查找成本。

终止开关

有时事情会出错。当它们出错时,您需要有一种方法立即使整个会话类无效。因为生成哈希或签名需要服务器端的秘密或密钥,替换秘密将立即导致所有会话 ID 验证失败。通过对不同类型的会话 id 使用不同的秘密,可以隔离和管理整个会话类。如果没有这样的机制,应用程序本身必须对每个会话的状态做出计算决策或执行大量数据库更新。

此外,在具有跨不同地理位置的数据库复制的大型分布式系统中,使一个位置的会话记录无效可能需要几秒钟甚至几分钟的时间来复制。这意味着会话保持活动状态,直到整个系统重新同步。与自我描述和自我验证的会话 id 相比,好处是显而易见的。

一般用途

Express 会话中间件的一个重要特性是它支持用户生成的会话 ID。这允许开发人员在现有环境中部署中间件,其中会话 ID 由可能驻留在完全不同平台上的现有实体生成。如果不向用户提供的会话 id 添加哈希,构建安全系统的负担就会从专家(模块作者)转移到用户(可能是安全新手)。应用哈希是比强制内部会话 id 生成器更好的方法。

鳄鱼

将哈希添加到强随机会话 ID 并不是您应该做的全部。您的护城河是否可以从鳄鱼中受益,这又是一个特定于城堡的决定。不用离题太远,您可以将许多其他层添加到会话管理层。例如,您可以使用两个会话凭据,一个是长期存在的(与会话一样长),另一个是短期的(适用于几分钟或几小时)。要刷新短寿命,您可以使用长寿命,但这样做,您正在减少网络上长寿命凭证的暴露(尤其是在不使用 TLS 时)。

另一种常见的做法是在会话旁边放置一个包含用户一般信息(例如,名字、最近查看的项目等)的 cookie,然后在哈希中包含来自该 cookie 的内容,以在用户的​​活动状态与身份验证。这是一种将“用户名”带回工作流程的方法。

更进一步,可以用签名代替散列,并且可以加密 cookie 内容(然后在顶部进行散列或签名)。安全详细程度必须与威胁相匹配。

Eran Hammer 是 NodeJS 的yar会话管理模块的维护者之一,他对此事有这样的看法:

免责声明:就像不了解您自己系统细节的人提供的任何安全建议一样,这仅用于教育目的。安全性是一个复杂且非常具体的领域,如果您担心系统的安全性,您应该聘请一位专家来审查您的系统并对其进行威胁分析并提供适当的建议。

[...]为内部完整性和验证签署不记名令牌是常见的安全做法。虽然我不认为 express-session 做得足够,但它肯定是最低要求。

您不希望人们能够猜测会话以访问其他人的帐户。[...]通过包含[sic]加密组件 [sic]来防止猜测会话 ID是实现此目的的正确方法。这是行业标准。

会话 cookie 是不记名令牌,这意味着拥有它们的任何人都可以作为合法所有者获得完全访问权限。您希望能够做一些事情来降低风险。确保除了服务器之外没有其他人可以发布有效的凭据是最低限度的,这就是在快速会话中所做的。

我不认为简单的签名足以保护会话管理,这就是为什么这个模块 [yar] 使用 编码,它既加密内容又对其进行签名以进行完整性验证。这使得任何人几乎不可能对状态进行调整(以及提供在 cookie 本身中维护某些状态的能力),但更重要的是,它允许您为凭据设置过期时间。

虽然 cookie 带有过期说明,但攻击者不需要遵守。这意味着如果有人窃取了 cookie,如果无法强制该 cookie 短暂存在,它将使系统暴露很长时间。使 cookie 过期策略与服务器端策略保持同步是另一层复杂性,不应成为系统安全配置文件的一部分。

这里还有很多其他的攻击媒介需要考虑。例如,如果每个请求都触发了会话存储查找,则使用无效会话 ID更容易进行DOS 。如果您有一种快速检测请求来自攻击者的方法,您既可以阻止它,也可以采取措施对抗它。无效的 cookie 签名只能来自恶意来源(除非您更改了生成 cookie 的方式,在这种情况下您知道并且可以处理它)。

同样,能够判断会话 id 是伪造还是过期对于维护安全系统至关重要。如果您只是生成随机 id,假设它们足够长且足够随机,那么除非您跟踪曾经生成的每个会话 id,否则您无法知道,这将使扩展系统成本高昂且痛苦。并且应该很明显地告诉您为什么要知道错误的 cookie 请求是否来自攻击者。

(查看完整评论)

所以基本上,虽然将随机会话 ID 直接存储在 cookie 中不一定不安全(只要 ID 不可预测且足够长),但对会话信息进行加密和签名提供了许多附加功能(例如存储过期的能力cookie 本身中的信息,以及识别伪造会话 ID 尝试的能力),可用于提高安全性。

特别是对于 expressjs,当前的维护者还提供了以下理由

用户可以使用他们想要的任何会话 ID。也许有人觉得他们应该使用 SQL 数据库中的自动递增数字,这没关系,因为我们通过签署值来保护他们不知情的决定,延长密钥。

换句话说,签署会话 ID 可以保护库的用户,这些用户可能(不正确地)在其应用程序中使用可预测或不够长的会话 ID(而不是让库处理会话 ID 的生成)。显然,如果用户使用安全选择的会话 ID,则此原因不适用,但该库选择不做出该假设。

Hammer重申了这一点

此外,这是一个通用模块,假设它的核心需求是广泛的用户。它绝对必须假设有些人会使用不好(例如增加 ids)并适应它。这也是为广大受众构建模块时的常见做法。