什么是“安全”的 URL 缩短算法?

信息安全 哈希 网址
2021-08-22 12:17:56

情况:目前,我们正在向我们的用户发送电子邮件和短信,其中包括指向每个用户必须定期填写的表格(“电子日记”)的链接。由于我们需要能够相应地对用户进行身份验证和授权,因此我们目前将 jwt 令牌作为查询参数附加到链接,因为我们希望使用户尽可能容易,因为他们中的技术含量不多精明。

这样做的问题是,链接往往因此很长,这使得它非常昂贵,因为我们发送了大约五个 SMS,其中四个仅用于链接。

因此,我们目前正在寻找一种以安全方式缩短链接的方法。由于正在输入的数据非常敏感,因此链接应该仍然足够安全,因此无法在“短”时间内猜到。这些链接通常在一两天内有效,因为我们不能“强迫”用户在更短的时间内输入数据。

因此,我很可能正在寻找一种可以在这种情况下使用的散列算法,其中链接的结构是众所周知的。当然,我们还会限制用户在被系统暂时阻止之前可以进行的重试次数,但如果我们必须提供数百万个链接,那么随机猜测一个的机会就会增加。

问题:在尽可能缩短 URL 和保持链接足够安全以便特定用户和所有用户都不太可能“猜测”特定链接(生日攻击)之间,什么是一个很好的折衷方案?

3个回答

熵是你的朋友。仅使用字母数字字符(在这种情况下最好避免使用特殊字符,因为它们通常需要 URL 编码,这会使事情变得复杂)您有 62 个可能的字符的“语言”可供选择。对于由这种“语言”组成的长度字符串X,可能的字符串总数很简单:

62**X

如果您在Y尝试失败后开始阻止 IP 地址,那么具有单个 IP 地址的攻击者猜测代码的几率是:

Y/(62**X)

但是假设攻击者可以轻松切换 IP 地址,那么让我们假设他们有 100 万个 IP 地址可供使用(注意:如果您支持 IPV6,这个数字会更大)。因此,他们成功的几率很简单:

(1e6*Y)/(62**X)

最后请注意(h/t @Falco)以上假设攻击者正在寻找特定代码。如果您担心有人找到任何代码,那么您需要进一步乘以您在给定时间拥有的活动代码的数量,这取决于它们的创建频率和过期速度。

考虑到所有这些,您只需要确定您希望概率有多低,插入您的 Y,然后求解 X。作为一个简单的起点,我通常建议使用 32 个字符的字母数字字符串(确保并使用正确的CSPRNG)。如果您在 1000 次尝试失败后阻止 IP,那么攻击者找到特定代码的几率为:

(1e6*1000)/(62**32)

这是4.400134339715791e-49考虑到这些几率,攻击者更有可能在猜出代码之前连续中奖 4 或 5 次。您一次可能有数十亿个活动代码,而猜测任何一个的几率实际上仍然为零。

TL;DR:不要为速率限制而烦恼。只需使用您首选的加密 API/库为每个 URL 生成一个安全的随机 128 位(或 192 位)令牌,然后使用base64url 对其进行编码。在 URL 中包含编码的令牌,并将其与关联的用户、表单和过期数据一起存储在安全数据库中。


像 Conor Mancone 一样,我也建议在 URL 中包含一个具有足够熵的单个随机令牌。您显然应该使用加密安全的随机数源来生成这些令牌。

生成 URL 时,您应该将每个令牌连同验证用户身份和显示正确表单所需的任何相关信息一起存储在数据库中。您可能还希望存储创建和/或过期时间戳,以限制 URL 的有效期(从而降低旧电子邮件被泄露的风险),还只是为了允许您从数据库中清除旧记录.

至于什么算作“足够的熵”,精确的下限显然取决于您的用例和威胁模型。特别是,假设您希望在任何给定时间在您的数据库中最多有 2 p个有效 URL,那么您的对手最多可以对您的服务进行 2 q查询,并且他们最多应该有 1-in-2 r成功猜测有效 URL 的机会,您的令牌应该至少p + q + r 位长。

实际上,相当安全的“行业标准”令牌长度为 128 位。假设您一次最多拥有 2 32 个有效 URL,则 128 位令牌将要求攻击者对您的服务进行至少 2 64次查询,以便有 1/2 32的机会猜到一个有效的 URL . 对于大多数目的,即使没有任何速率限制,这也应该绰绰有余。

(切线,128 位令牌长度还允许您在平均遭受第一次令牌冲突之前生成最多约 2 64 个随机令牌。但这有点无关紧要,因为无论如何数据库都允许您检测冲突并处理他们只是通过生成一个新的令牌。)

如果你真的想确定,你可以达到 192 甚至 256 位。例如,一个 192 位的令牌将允许您拥有多达 2 64 个URL,同时需要至少 2 64 个查询才能获得 1/2 64的攻击成功概率并且 256 位令牌会在此基础上将攻击的难度增加 2 64倍——我并不认为这对于任何现实威胁都是必要的。

至于生成和编码令牌,我建议使用您选择的任何加密 RNG 简单地生成一个随机的 128 位(或 192 位或 256 位)位串,并使用URL-safe Base64对其进行编码。(大多数编程语言运行时应该内置合适的 RNG,或者至少作为库易于安装。如果没有,您的操作系统很可能会提供一个,例如/dev/urandom在 Unixish 系统上。)这将为一个 128 位标记,一个 32 个字符的字符串用于 192 位标记或一个 43 个字符的字符串用于 256 位标记。正如Conor Mancone的回答所暗示的那样,它比一次生成一个字符的令牌要简单得多。


顺便说一句,如果您碰巧无法访问方便的数据库和/或安全的 RNG,另一种选择是在 URL 本身中包含所有必要的信息(至少是用户 ID、表单 ID 和时间戳)以及这些值的128 位加密消息验证码(使用存储在服务器上的密钥计算和验证)。事实上,这基本上就是JWT对令牌进行身份验证所做的事情,只是需要更多的开销。

请注意,在这种特殊情况下,每个令牌仅对单个用户/表单/时间戳组合有效,攻击者在尝试猜测令牌之前必须选择它,因此有效地p = 0(因为 2 0 = 1)。因此,与使用前面描述的随机令牌方法相比,稍短一些的令牌可以提供相同的有效安全级别。当然,这种长度节省通常通过需要包含在 URL 中的额外参数来平衡。

如果你想要安全,我会推荐使用 base58 编码的 UUIDv4。本质上,您会得到 22 个 URL 安全的字母数字字符,并且它们存储完整的 UUIDv4,这(合理地)保证是随机且不可猜测的。

关于这个主题的一篇很好的文章:https : //www.skitoy.com/p/base58-unique-ids/638/