事件溯源和密码更改安全隐患
Event Sourcing and password change security implications
我最近开始致力于 事件溯源,当出现密码更新的问题时。
我的理解如下:
事件存储在 事件存储 中,它充当当前应用程序和对象状态的单一真实来源。我们可能会重播给定对象自创建该对象以来的一系列事件,并找到该对象的当前状态。
事件必须无限期存储,因为链中的中断会导致潜在的不一致状态。如果某个视图每次处理的事件太多,我们可能会制作事件链(即对象的当前状态)的快照。
对我来说,当涉及到 user updated password
这样的事情时,这具有明显的安全隐患。在这种情况下,我们会看到如下内容:
- UserCreatedEvent(user)
- ... // other events that might change the state of the User object
- UserChangedPasswordEvent(updatedPassword)
我发现这种方法的问题是,为了让应用程序保持一致的状态,我们必须存储 用户的所有先前密码,因为我们无法判断给定的是否是password 是当前的一个或只是用户以前的密码之一(仅给出 UserChangedPasswordEvent
)。
为了争论起见,假设应用程序使用 BCrypt
以外的较弱算法存储密码,并且密码在给定的时间范围后是可破解的(即蛮力/彩虹 table).
在事件溯源的情况下,设法获取 UserChangedPasswordEvent
商店的攻击者现在将拥有 所有 用户拥有的密码的列表曾经在应用程序中使用过。在这种情况下,他们不太可能访问 UserCreatedEvent
存储,因此也可以访问用户的(通常)唯一的电子邮件。
由于不幸的是大多数临时用户在各种平台上重复使用密码,因此攻击者现在可能可以访问用户可能曾经在多个平台上使用过的任意数量的密码。如果存在 'mandatory password renewal after X time'.
这样的机制,情况会变得更糟
尽管如此,这是最常见的事件源和密码更新方法,还是有标准化的方法来处理这部分应用程序?我承认场景的前提(弱密码散列)很弱,但它最能说明我的观点。
我可以想到两种方法来处理这个问题:
- 加密事件存储and/or文件系统;影响性能
UserChangedPasswordEvent
仅通知更改本身,密码通过其他渠道存储在其他地方;然而,这与事件溯源的想法背道而驰)
我是不是想多了? 这里有问题吗,如果使用了正确的散列算法?
的确,将密码存储在事件中是一件非常危险的事情,但您没有真正的理由将密码存储在事件负载中。事实上,您甚至可能不会为 UserCredentialsSubdomain
或整个 AuthenticationDomain
.
使用事件源
如果您仍然决定为 AuthenticationDomain
使用事件源(这不一定是坏事),则不需要将密码存储在 UserChangedPasswordEvent
中,因为您不需要Write 模型中的整个密码历史记录(散列或明文)。最后一个密码(或散列)仅由身份验证服务用于验证用户的身份。没有其他 Read 模型需要这个;您需要最近的密码历史记录(即不允许更改为旧密码)的用例可以使用密码日志或类似的东西来实现,您不需要为此进行事件溯源。 UserChangedPasswordEvent
可能很有用,例如向用户显示更改密码的最后日期,但不包含密码本身。
评论后更新:
您不需要为整个应用程序使用 ES,它不是所有 ES 或所有非 ES。通常,对于身份验证,人们使用平面模型。但是如果你选择使用 ES,你仍然可以使用它来重建 UserAggregate 的状态,即使你没有用户的密码,因为你实际上并不需要这个状态中的密码。
在我所指的这种情况下,密码检查是在 UserAggregate(事件流的所有者)处理 LoginCommand 之前完成的,方法是调用使用平面持久性的 PasswordCheckingService。这是在应用层完成的:首先检查密码,然后由 UserAggregate 进一步检查登录(即,如果用户仍然处于活动状态,则可以登录)。
事实上,从事件流中排除密码会让您意识到验证用户身份应该是一个单独的子系统,以及通过 phone 或生物识别扫描进行的验证。在我看来,这会稍微改变您的架构,使其更加清晰。整个身份验证可以隐藏在具有多个实现的简单接口之后。
Wouldn't the scenario you describe explicitly make some other kind of call
不,它不会或至少不会重建聚合的状态。该远程调用将在应用层完成。
重建状态时调用外部服务是违反事件源的-事件流必须足够。
从根本上说,存储密码历史与存储一个密码并没有太大区别 - 会出现相同类型的问题。
据我所知,没有一种 "standardized way" 可以处理事情,但不同的人正在尝试多种不同的方法
将实际密码存储在事件流之外。
在事件中存储密码,但加密该值,使其仅对授权进程可用。
现在这是一个相当热门的话题,因为 GDPR; what does it mean to store secrets in a persistent data store when data subjects have a right to erasure。
我看到的最常讨论的实现是为每个数据主体使用唯一的加密密钥,因此如果您需要 "erase" 数据,您可以丢弃该密钥。 (注意:尚不清楚这种方法是否会令法院满意。)
更广泛地说,需要记住 "events" 服务于多种目的,我们不需要为两者使用相同的事件或相同的表示。你有 "private" 持久性存储,它允许 this 域模型重建它自己的状态,然后你有 "public" 表示,为由其他服务在他们认为合适的情况下使用。
例如,作为密码管理服务,我可能会向全世界广播 Bob 更改密码的事件,但 不会 将新密码包含在 public表示。
点评乌迪大汉on service boundaries, or his talk on finding service boundaries in health care。
我最近开始致力于 事件溯源,当出现密码更新的问题时。
我的理解如下:
事件存储在 事件存储 中,它充当当前应用程序和对象状态的单一真实来源。我们可能会重播给定对象自创建该对象以来的一系列事件,并找到该对象的当前状态。
事件必须无限期存储,因为链中的中断会导致潜在的不一致状态。如果某个视图每次处理的事件太多,我们可能会制作事件链(即对象的当前状态)的快照。
对我来说,当涉及到 user updated password
这样的事情时,这具有明显的安全隐患。在这种情况下,我们会看到如下内容:
- UserCreatedEvent(user)
- ... // other events that might change the state of the User object
- UserChangedPasswordEvent(updatedPassword)
我发现这种方法的问题是,为了让应用程序保持一致的状态,我们必须存储 用户的所有先前密码,因为我们无法判断给定的是否是password 是当前的一个或只是用户以前的密码之一(仅给出 UserChangedPasswordEvent
)。
为了争论起见,假设应用程序使用 BCrypt
以外的较弱算法存储密码,并且密码在给定的时间范围后是可破解的(即蛮力/彩虹 table).
在事件溯源的情况下,设法获取 UserChangedPasswordEvent
商店的攻击者现在将拥有 所有 用户拥有的密码的列表曾经在应用程序中使用过。在这种情况下,他们不太可能访问 UserCreatedEvent
存储,因此也可以访问用户的(通常)唯一的电子邮件。
由于不幸的是大多数临时用户在各种平台上重复使用密码,因此攻击者现在可能可以访问用户可能曾经在多个平台上使用过的任意数量的密码。如果存在 'mandatory password renewal after X time'.
这样的机制,情况会变得更糟尽管如此,这是最常见的事件源和密码更新方法,还是有标准化的方法来处理这部分应用程序?我承认场景的前提(弱密码散列)很弱,但它最能说明我的观点。
我可以想到两种方法来处理这个问题:
- 加密事件存储and/or文件系统;影响性能
UserChangedPasswordEvent
仅通知更改本身,密码通过其他渠道存储在其他地方;然而,这与事件溯源的想法背道而驰)
我是不是想多了? 这里有问题吗,如果使用了正确的散列算法?
的确,将密码存储在事件中是一件非常危险的事情,但您没有真正的理由将密码存储在事件负载中。事实上,您甚至可能不会为 UserCredentialsSubdomain
或整个 AuthenticationDomain
.
如果您仍然决定为 AuthenticationDomain
使用事件源(这不一定是坏事),则不需要将密码存储在 UserChangedPasswordEvent
中,因为您不需要Write 模型中的整个密码历史记录(散列或明文)。最后一个密码(或散列)仅由身份验证服务用于验证用户的身份。没有其他 Read 模型需要这个;您需要最近的密码历史记录(即不允许更改为旧密码)的用例可以使用密码日志或类似的东西来实现,您不需要为此进行事件溯源。 UserChangedPasswordEvent
可能很有用,例如向用户显示更改密码的最后日期,但不包含密码本身。
评论后更新:
您不需要为整个应用程序使用 ES,它不是所有 ES 或所有非 ES。通常,对于身份验证,人们使用平面模型。但是如果你选择使用 ES,你仍然可以使用它来重建 UserAggregate 的状态,即使你没有用户的密码,因为你实际上并不需要这个状态中的密码。
在我所指的这种情况下,密码检查是在 UserAggregate(事件流的所有者)处理 LoginCommand 之前完成的,方法是调用使用平面持久性的 PasswordCheckingService。这是在应用层完成的:首先检查密码,然后由 UserAggregate 进一步检查登录(即,如果用户仍然处于活动状态,则可以登录)。
事实上,从事件流中排除密码会让您意识到验证用户身份应该是一个单独的子系统,以及通过 phone 或生物识别扫描进行的验证。在我看来,这会稍微改变您的架构,使其更加清晰。整个身份验证可以隐藏在具有多个实现的简单接口之后。
Wouldn't the scenario you describe explicitly make some other kind of call
不,它不会或至少不会重建聚合的状态。该远程调用将在应用层完成。
重建状态时调用外部服务是违反事件源的-事件流必须足够。
从根本上说,存储密码历史与存储一个密码并没有太大区别 - 会出现相同类型的问题。
据我所知,没有一种 "standardized way" 可以处理事情,但不同的人正在尝试多种不同的方法
将实际密码存储在事件流之外。
在事件中存储密码,但加密该值,使其仅对授权进程可用。
现在这是一个相当热门的话题,因为 GDPR; what does it mean to store secrets in a persistent data store when data subjects have a right to erasure。
我看到的最常讨论的实现是为每个数据主体使用唯一的加密密钥,因此如果您需要 "erase" 数据,您可以丢弃该密钥。 (注意:尚不清楚这种方法是否会令法院满意。)
更广泛地说,需要记住 "events" 服务于多种目的,我们不需要为两者使用相同的事件或相同的表示。你有 "private" 持久性存储,它允许 this 域模型重建它自己的状态,然后你有 "public" 表示,为由其他服务在他们认为合适的情况下使用。
例如,作为密码管理服务,我可能会向全世界广播 Bob 更改密码的事件,但 不会 将新密码包含在 public表示。
点评乌迪大汉on service boundaries, or his talk on finding service boundaries in health care。