本文介绍了从UUID或HMAC/JWT/hash生成一次性安全令牌?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

限时删除!!

我正在为Web应用程序构建后端.当新用户访问该网站并点击注册按钮时,他们将填写一个超级简单的表格,要求他们提供用户名和密码,然后提交.这会提示服务器将验证电子邮件发送到该电子邮件地址.然后,他们将检查其电子邮件,单击链接(验证其电子邮件),然后将其路由到登录页面,以便他们选择登录.

I'm build the backend for a web app. When a new user goes to the site and clicks the Sign Up button, they'll fill out a super simple form asking them for their username + password and they'll submit. This prompts the server to send a verification email to that email address. They'll then check their email, click a link (which verifies their email) and then be routed to the login page so they can sign in if they choose.

为了验证其电子邮件,服务器生成电子邮件时,它将需要创建(并存储)验证令牌(可能是UUID)并将其附加到电子邮件中的此链接,这样链接看起来就像:

In order to verify their email, when the server generates the email it will need to create (and store) a verification token (likely a UUID) and attach it to this link in the email, so that the link looks something like:

其中vt=12345是验证令牌"(同样可能是UUID).因此,用户单击此链接,我的GET v1/users/verify端点查看令牌,以某种方式确认其有效,并进行一些数据库更新以激活"用户.他们现在可以登录.

Where vt=12345 is the "verification token" (again likely a UUID). So the user clicks this link and my GET v1/users/verify endpoint looks at the token, somehow confirms its valid, and makes some DB updates to "activate" the user. They can now log in.

类似的情况,例如当用户希望退订接收电子邮件或无法记住密码而需要恢复密码以便登录时.

Similar scenarios for when a user wants to unsubscribe from receiving email, or when they can't remember their password and need to recover it so that they can log in.

用户希望停止接收电子邮件,但仍想使用该应用程序.他们在我们发送给他们的每周新闻简报中单击"退订"链接.该链接需要包含某种类似的退订令牌",就像上面的验证令牌一样,是在服务器上生成并存储的,用于验证用户的退订电子邮件请求.

User wants to stop receiving emails but still wants to use the app. They click an "Unsubscribe" link in a weekly newsletter we send them. This link needs to contain some kind of similar "unsubscribe token" that, like the verification token above, is generated + stored on the server, and is used to authenticate the user's request to unsubscribe from email.

此处用户忘记了密码,需要恢复它.因此,在登录屏幕上,他们单击"忘记我的密码"链接,并显示一个表单,必须在该表单中填写其电子邮件地址.服务器将电子邮件发送到该地址.他们检查此电子邮件,其中包含指向表单的链接,可以在其中输入新密码.该链接需要包含一个重置密码令牌",就像上面的验证令牌一样,该密码是生成并存储在服务器上的,用于验证用户更改密码的请求.

Here the user has forgotten their password and needs to recover it. So at the login screen they click the "Forgot my password" link, and are presented with a form where they must fill out their email address. Server sends an email to that address. They check this email and it contains a link to a form where they can enter their new password. This link needs to contain a "reset password token" that -- like the verification token above -- is generated + stored on the server, and is used to authenticate the user's request to change their password.

因此,这里我们要解决三个非常相似的问题,所有这些都需要使用我所说的"一次性(OTO)安全令牌".这些OTO令牌:

So here we have three very similar problems to solve, all requiring the use of what I'm calling "one-time only (OTO) security tokens". These OTO tokens:

  • 必须在服务器端生成并保留(也许到security_tokens表)
  • 必须是可以附加到我们将从电子邮件内部公开的链接的东西
  • 必须仅有效一次:他们单击它后,令牌将被使用"并且不能重复使用
  • Must be generated server-side and persisted (maybe to a security_tokens table)
  • Must be something that can be attached to links that we'll expose from inside of emails
  • Must only be valid one time: once they click it, the token is "used" and cannot be reused

我想出的解决方案很简单...几乎太简单了.

The solution I came up was simple...almost too simple.

对于令牌,我只是生成随机UUID(36个字符)并将其存储到具有以下字段的security_tokens表中:

For the tokens I am just generating random UUIDs (36-char) and storing them to a security_tokens table that has the following fields:

[security_tokens]
---
id (PK)
user_id (FK to [users] table)
token (the token itself)
status (UNCLAIMED or CLAIMED)
generated_on (DATETIME when created)

服务器创建它们时,它们是未声明的".用户单击表内的链接时,将被清除".后台工作作业将定期运行,以清理所有已过期"的CLAIMED令牌或删除所有已过期"的UNCLAIMED令牌(基于其generated_on字段).该应用程序还将忽略之前已清除(并且尚未清除)的所有令牌.

When the server creates them they are "UNCLAIMED". When the user clicks a link inside the table they are "CLAIMED". A background worker job will run periodically to clean up any CLAIMED tokens or to delte any UNCLAIMED tokens that have "expired" (based on their generated_on fields). The app will also ignore any tokens that have been previously CLAIMED (and have just not yet been cleaned up).

认为该解决方案可以工作,但我不是超级安全专家,我担心这种方法:

I think this solution would work, but I'm not a super security guy and I'm worried that this approach:

  1. 可能会使我的应用对某些类型的攻击/利用开放;和
  2. 在某些其他解决方案可能同样有效的情况下,可能会重新发明轮子

就像上面的第二个例子一样,我想知道是否应该使用与哈希/HMAC/JWT相关的机制,而不要使用完全无效的简单UUID.也许有一些聪明的加密/安全人员找到了一种使这些令牌以安全/不变的方式自己包含CLAIM状态和有效期的方法,等等.

Like for the 2nd one above I'm wondering if I should be using a hash/HMAC/JWT-related mechanism instead of a dead simple UUID. Maybe there's some smart crypto/security folks who found a way to make these tokens contain CLAIM status and expiration date themselves in a secure/immutable fashion, etc.

推荐答案

您在正确的位置

根据我希望它执行的操作,我的应用程序中有一个非常相似的方法.我有一个包含每个用户的表(一个Users表),可以用来引用每个单独的帐户并根据其身份执行操作.通过添加用户帐户和自我管理选项,可以缓解许多安全威胁.这是我如何解决其中一些漏洞的方法.

I have a very similar method in my application based on what I want it to do. I have a table containing each user (a Users table) which I can use to reference each individual account and perform actions based on their identity. There are a lot of security threats to mitigate by adding in user accounts and self-management options. Here's how I combat a few of these vulnerabilities.

验证您的电子邮件

当用户注册时,服务器应使用RNGCryptoServiceProvider()类生成随机盐,其长度足够长,以至于无法实际猜测.然后,我对盐进行哈希处理(单独使用)并对其应用base64编码,以便可以将其添加到Url中.通过电子邮件将完整的链接发送给用户,并确保将哈希值与相关的UserId存储在Users表中.

When a user signs up, the server should use the RNGCryptoServiceProvider() class to generate a random salt with sufficient length that it could never realistically be guessed. Then, I hash the salt (on it's own) and apply base64 encoding to it so that it can be added to a Url. Send the completed link to the user via email, and be sure to store that hash against the relevant UserId in the Users table.

用户在收件箱中看到一个漂亮整洁的单击此处以验证您的电子邮件地址",并且可以单击链接.它应该重定向到一个接受可选url参数(例如mywebsite.com/account/verifyemail/myhash)的页面,然后检查哈希服务器端.然后,该站点可以根据存储在数据库中的激活哈希值检查哈希值.如果它与记录匹配,则应将Users.EmailVerified列标记为true并提交到表中,然后可以从表中删除该Verification记录条目.

The user sees a nice and neat "Click here to validate your email address" in their inbox and can click on the link. It should redirect to a page that accepts an optional url parameter (such as mywebsite.com/account/verifyemail/myhash and then check the hash server-side. The site can then check the hash against the activation hashes it has stored in the database. If it matches a record, then you should mark the Users.EmailVerified column to true and commit to the table. Then, you can delete that Verification record entry from the table.

做得好,您已经成功验证了用户的电子邮件地址是真实的!

Well done, you've successfully verified a user's email address is real!

重置密码

在这里,我们实现了类似的方法.但是最好不要将验证记录存储在PasswordResetRequest表中,并且不要删除记录-这使您可以查看是否重置了密码以及何时重置密码.每次用户请求重设密码时,您都应显示一条匿名消息,例如一封电子邮件已发送到您的主电子邮件地址,其中包含进一步的说明".即使未发送或帐户不存在,它也阻止了潜在的攻击者枚举用户名或电子邮件地址,以查看它们是否已在您的服务中注册.同样,如果它们是真实的,则使用与以前相同的方法发送链接.

Here, we implement a similar method. But instead of a Verification record, we're better off storing our record in a PasswordResetRequest table, and do not delete records - this allows you to see whether or not a password was reset and when. Each time the user requests a password reset, you should display an anonymous message such as "An email was sent to your primary email address containing further instructions". Even if one was not sent or the account doesn't exist, it stops a potential attacker from enumerating usernames or email addresses to see if they are registered with your service. Again, if they are real, send a link using the same method as before.

用户打开其电子邮件地址,然后单击链接.然后将它们重定向到重置页面,例如mywebsite.com/account/resetpassword/myhash.然后,服务器对数据库运行url中的哈希,如果结果是真实的,则返回结果.现在,这是棘手的部分-您不应该长时间保持这些活动.我建议您使用一列将散列链接到Users.UserId,一个称为ExpiraryDateTime的列,其中包含类似Datetime.Now.AddMinutes(15)的内容(这使得以后使用起来更容易),而另一个称为IsUsed的内容则是布尔值(false表示)默认).

The users opens their email address and clicks on the link. They are then redirected to a reset page such as mywebsite.com/account/resetpassword/myhash. The server then runs the hash in the url against the database and returns a result if it is real. Now, this is the tricky part - you shouldn't keep these active for long. I'd recommend a column linking the hash to the Users.UserId, one called ExpiraryDateTime which contains something like Datetime.Now.AddMinutes(15) (which makes it easier to work with later), and one called IsUsed as a boolean (false by default).

在单击链接时,应检查链接是否存在.如果不是,请为他们提供默认的该链接存在问题.请请求一个新的"文本.但是,如果链接有效,则应检查Used == false,因为您不希望其他人多次使用同一链接.如果不使用,那就太好了!让我们检查一下它是否仍然有效.最简单的方法是简单的if (PasswordResetRequest.ExpiraryDateTime < DateTime.Now)-如果链接仍然有效,则可以继续进行密码重置.如果不是,则表示它是前一段时间生成的,您不应再使用它.严重的是,某些站点仍然允许您今天生成链接,如果您的电子邮件从现在起1个月被黑,您仍然可以使用重置链接!

On clicking a link, you should check to see if a link exists. If not, give them them to the default "There was a problem with that link. Please request a new one" text. However, if the link is valid, you should check that Used == false because you don't want people using the same link more than once. If it's not used, great! Let's check to see if it's still valid. The easiest way would be a simple if (PasswordResetRequest.ExpiraryDateTime < DateTime.Now) - if the link is still valid, then you can proceed with the password reset. If not, it means it was generated a while ago and you shouldn't allow it to be used anymore. Seriously, some sites will still allow you to generate a link today and if your email is hacked 1 month from now, you can still use the reset links!

我还应该提到,每次用户请求重设密码时,都应检查表中的现有记录,以获取有效链接.如果一个有效(意味着它仍然可以使用),那么您应该立即使其无效.将散列替换为一些辅助文字,例如无效:用户请求了新的重置链接".这也可以让您知道他们请求了多个链接,同时也使他们的链接无效.如果您确实只是想通过智能地阻止人们尝试使用过期的链接,也可以将其标记为已使用",并将整个无效:用户请求新的重置链接"作为编码的URL潜入他们的浏览器.对于同一帐户,您永远不能有多个激活的重置链接-永远

I should also mention that each time the user requests a password reset, you should check the existing records in the table for a valid link. If one is valid (meaning it can still be used) then you should instantly invalidate it. Replace the hash with some assistive text like "Invalid: User requested new reset link". This also lets you know they've requested more than one link whilst also invalidating their link. You could also mark it as Used if you really wanted to just to prevent people from trying to use expired links by being smart and sneaking the whole "Invalid: User requested new reset link" as an encoded URL into their browsers. You should never have more than one reset link active for the same account - ever!

退订

为此,我在数据库中有一个简单的标志,用于确定用户是否可以接收促销优惠和新闻通讯等.因此Users.SubscribedToNewsletter就足够了.他们应该能够登录并在其电子邮件设置"或通信首选项"等中进行更改.

For this, I'd have a simple flag in the database that determines whether or not a user can receive promotional offers and newsletters etc. So a Users.SubscribedToNewsletter would suffice. They should be able to log in and change this in their Email Settings or Communication Preferences etc.

一些代码示例

这是我在C#中的RNGCryptoServiceProvider代码

This is my RNGCryptoServiceProvider code in C#

public static string GenerateRandomString(RNGCryptoServiceProvider rng, int size)
{
    var bytes = new Byte[size];

    rng.GetBytes(bytes);

    return Convert.ToBase64String(bytes);
}

var rng = new RNGCryptoServiceProvider();
var randomString = GenerateRandomSalt(rng, 47); // This will end up being a string of almost entirely random bytes

我为什么要使用RNGCryptoServiceProvider?

RNGCryptoServiceProvider()(在其Security库中是C#类)允许您基于完全随机和不可再现的事件生成看似随机的字节字符串.诸如Random()之类的类仍需要使用某种内部数据来基于可预测的算法事件(例如当前日期和时间)来生成数字. RNGCryptoServiceProvider()使用诸如CPU温度,正在运行的进程数之类的东西来创建无法复制的随机事物.这样可以使最终的字节数组尽可能地随机.

The RNGCryptoServiceProvider() (which is a C# class in their Security library) allows you to generate a seemingly random string of bytes based on entirely random and non-reproducable events. Classes like the Random() still need to use some sort of internal data to generate a number based on predictable algorithmic events such as current date and time. The RNGCryptoServiceProvider() uses things like cpu temperatures, number of running processes, etc. all to create something random that can't be reproduced. This allows for the final byte array to be as random as possible.

为什么我要对Base64进行编码?

Base64编码将导致仅包含数字和字母的字符串.这意味着文本内将没有符号或编码字符,因此可以安全地在URL中使用.这并不是什么安全性功能,但是它确实允许您在方法的参数内仅允许数字和字母,并过滤或拒绝任何不符合此标准的输入.例如,过滤掉包含人字形<>的所有输入,应该可以防止XSS.

Base64 encoding will result in a string containing only numbers and letters. This means there will be no symbols or encoded characters within the text and therefore it is safe to use in a URL. This isn't so much a security feature, but it does allow you to only allow numbers and letters within the parameters of the method, and filter out or reject any input that doesn't match this standard. For example, filtering out any inputs that contain the chevrons < and > should allow you to prevent XSS.

需要注意的地方

您应该始终假设包含您的哈希的链接为无效,直到您对其进行每次检查以确保其通过要求为止.因此,您可以执行各种if语句,但是除非您通过每个单独的语句,否则您都会将默认的 next动作留给用户某种形式的错误.为了明确起见,我应该检查密码重置链接是否有效,然后未使用,然后仍然在时间窗口内,然后执行我的重置操作.如果它无法通过这些要求中的任何一项,则默认操作应该是向用户显示一条错误消息,指出它是无效的链接.

You should ALWAYS assume that the link containing your hash is invalid until you perform each check on it to ensure it passes requirements. So you can do your various if statements but unless you pass every single one, you leave your default next action to some form of error for the user. To clarify, I should check that the password reset link is valid, then not used, then still within the time window, and then perform my reset actions. Should it fail to pass any of these requirements, the default action should be to give the user an error saying that it is an invalid link.

其他人的注意事项

由于我非常有信心,这不是唯一的方式,所以我想声明,这是我多年来所做的,并且从未让我失败过并且使我的公司经历了多次广泛的考验.但是,如果有人有更好/更安全的方法,请尽我所能,因为我很乐于了解更多信息.如果您对我提到的特定部分还有其他疑问或需要澄清,请告诉我,我会尽力提供帮助

Since I'm pretty confident this isn't the only way to do this, I'd just like to declare that this is how I've done it for years which has never failed me and has gotten my company through several extensive pentests. But if someone has a better / more secure way of doing so, please do shed some light as I'd be happy to learn more. If you have any further questions or need clarification on a particular part I mentioned, just let me know and I'll try my best to help

这篇关于从UUID或HMAC/JWT/hash生成一次性安全令牌?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

1403页,肝出来的..

09-06 15:58