Skip to main content

原型中毒背后的历史

以下是埃兰·哈默(Eran Hammer)撰写的文章。它在此处复制以供后人使用 经许可。它已从原始 HTML 源代码重新格式化为 Markdown 源代码,但其他方面保持不变。可以从上面的权限链接检索原始 HTML。

¥The following is an article written by Eran Hammer. It is reproduced here for posterity with permission. It has been reformatted from the original HTML source to Markdown source, but otherwise remains the same. The original HTML can be retrieved from the above permission link.

原型中毒背后的历史

¥History behind prototype poisoning

根据 Eran Hammer 的文章,该问题是由网络安全错误造成的。这也完美地说明了维护开源软件所需的努力和现有沟通渠道的局限性。

¥Based on the article by Eran Hammer,the issue is created by a web security bug. It is also a perfect illustration of the efforts required to maintain open-source software and the limitations of existing communication channels.

但首先,如果我们使用 JavaScript 框架来处理传入的 JSON 数据,请花点时间阅读有关 原型中毒 的一般信息,以及此问题的具体 技术细节。这可能是一个关键问题,因此我们可能需要首先验证你自己的代码。它专注于特定的框架,但是,任何使用 JSON.parse() 处理外部数据的解决方案都可能存在风险。

¥But first, if we use a JavaScript framework to process incoming JSON data, take a moment to read up on Prototype Poisoning in general, and the specific technical details of this issue. This could be a critical issue so, we might need to verify your own code first. It focuses on specific framework however, any solution that uses JSON.parse() to process external data is potentially at risk.

BOOM

Lob 的工程团队(长期以来慷慨支持我的工作!)报告了他们在我们的数据验证模块中发现的一个严重安全漏洞 - joi。他们提供了一些技术细节和建议的解决方案。

¥The engineering team at Lob (long time generous supporters of my work!) reported a critical security vulnerability they identified in our data validation module — joi. They provided some technical details and a proposed solution.

数据验证库的主要目的是确保输出完全符合定义的规则。如果不存在,则验证失败。如果通过了,我们就可以盲目地相信你正在使用的数据是安全的。事实上,从系统完整性的角度来看,大多数开发者将经过验证的输入视为完全安全,这一点至关重要!

¥The main purpose of a data validation library is to ensure the output fully complies with the rules defined. If it doesn't, validation fails. If it passes, we can blindly trust that the data you are working with is safe. In fact, most developers treat validated input as completely safe from a system integrity perspective which is crucial!

在我们的案例中,Lob 团队提供了一个示例,其中一些数据能够通过验证逻辑转义并在未被检测到的情况下通过。这是验证库可能存在的最严重的缺陷。

¥In our case, the Lob team provided an example where some data was able to escape by the validation logic and pass through undetected. This is the worst possible defect a validation library can have.

简而言之,原型

¥Prototype in a nutshell

为了理解这一点,我们需要了解一下 JavaScript 的工作原理。JavaScript 中的每个对象都可以有一个原型。它是来自另一个对象的 "inherits" 的一组方法和属性。我将继承放在引号中,因为 JavaScript 并不是真正的面向对象语言。它是一种基于原型的面向对象语言。

¥To understand this, we need to understand how JavaScript works a bit. Every object in JavaScript can have a prototype. It is a set of methods and properties it "inherits" from another object. I have put inherits in quotes because JavaScript isn't really an object-oriented language. It is a prototype- based object-oriented language.

很久以前,出于一些不相关的原因,有人认为使用特殊属性名称 __proto__ 来访问(和设置)对象的原型是个好主意。此后已被弃用,但仍然得到充分支持。

¥A long time ago, for a bunch of irrelevant reasons, someone decided that it would be a good idea to use the special property name __proto__ to access (and set) an object's prototype. This has since been deprecated but nevertheless, fully supported.

展示:

¥To demonstrate:

> const a = { b: 5 };
> a.b;
5
> a.__proto__ = { c: 6 };
> a.c;
6
> a;
{ b: 5 }

该对象没有 c 属性,但其原型有。验证对象时,验证库会忽略原型,仅验证对象自身的属性。这允许 c 通过原型潜入。

¥The object doesn't have a c property, but its prototype does. When validating the object, the validation library ignores the prototype and only validates the object's own properties. This allows c to sneak in via the prototype.

另一个重要部分是 JSON.parse()(语言提供的将 JSON 格式的文本转换为对象的实用程序)处理这个神奇的 __proto__ 属性名称的方式。

¥Another important part is the way JSON.parse() — a utility provided by the language to convert JSON formatted text into objects  —  handles this magic __proto__ property name.

> const text = '{"b": 5, "__proto__": { "c": 6 }}';
> const a = JSON.parse(text);
> a;
{b: 5, __proto__: { c: 6 }}

请注意 a 如何具有 __proto__ 属性。这不是原型参考。它是一个简单的对象属性键,就像 b 一样。正如我们从第一个示例中看到的,我们实际上无法通过赋值创建此键,因为这会调用原型魔法并设置实际原型。但是,JSON.parse() 设置了一个具有该有毒名称的简单属性。

¥Notice how a has a __proto__ property. This is not a prototype reference. It is a simple object property key, just like b. As we've seen from the first example, we can't actually create this key through assignment as that invokes the prototype magic and sets an actual prototype. JSON.parse() however, sets a simple property with that poisonous name.

就其本身而言,JSON.parse() 创建的对象是完全安全的。它没有自己的原型。它有一个看似无害的属性,恰好与内置的 JavaScript 魔法名称重叠。

¥By itself, the object created by JSON.parse() is perfectly safe. It doesn't have a prototype of its own. It has a seemingly harmless property that just happens to overlap with a built-in JavaScript magic name.

然而,其他方法就没那么幸运了:

¥However, other methods are not as lucky:

> const x = Object.assign({}, a);
> x;
{ b: 5}
> x.c;
6;

如果我们采用 JSON.parse() 先前创建的 a 对象并将其传递给有用的 Object.assign() 方法(用于将 a 的所有顶层属性浅拷贝到提供的空 {} 对象中),那么神奇的 __proto__ 属性 "leaks" 将成为 x 的实际原型。

¥If we take the a object created earlier by JSON.parse() and pass it to the helpful Object.assign() method (used to perform a shallow copy of all the top level properties of a into the provided empty {} object), the magic __proto__ property "leaks" and becomes x 's actual prototype.

惊喜!

¥Surprise!

如果你获得一些外部文本输入并使用 JSON.parse() 解析它,然后对该对象执行一些简单的操作(例如浅克隆并添加 id ),并将其传递给我们的验证库,它将通过 __proto__ 潜入而不被发现。

¥If you get some external text input and parse it with JSON.parse() then perform some simple manipulation of that object (e.g shallow clone and add an id ), and pass it to our validation library, it would sneak in undetected via __proto__.

Oh joi!

第一个问题当然是,为什么验证模块 joi 会忽略原型并让潜在的有害数据通过?我们问了自己同样的问题,我们立即想到的是 "这是疏忽"。一个错误 - 一个非常大的错误。joi 模块不应该允许这种情况发生。但…

¥The first question is, of course, why does the validation module joi ignore the prototype and let potentially harmful data through? We asked ourselves the same question and our instant thought was "it was an oversight". A bug - a really big mistake. The joi module should not have allowed this to happen. But…

虽然 joi 主要用于验证 Web 输入数据,但它也有大量用户群使用它来验证内部对象,其中一些对象具有原型。joi 忽略原型这一事实是有用的 "feature"。它允许验证对象自己的属性,同时忽略可能非常复杂的原型结构(具有许多方法和字面量属性)。

¥While joi is used primarily for validating web input data, it also has a significant user base using it to validate internal objects, some of which have prototypes. The fact that joi ignores the prototype is a helpful "feature". It allows validating the object's own properties while ignoring what could be a very complicated prototype structure (with many methods and literal properties).

joi 级别的任何解决方案都意味着破坏一些当前正在工作的代码。

¥Any solution at the joi level would mean breaking some currently working code.

正确的事情

¥The right thing

此时,我们正在研究一个极其严重的安全漏洞。就在史诗般的安全失败的上层。我们所知道的是,我们极其流行的数据验证库无法阻止有害数据,而且这些数据很容易被窃取。你需要做的就是将 __proto__ 和一些垃圾添加到 JSON 输入中,然后将其发送到使用我们的工具构建的应用。

¥At this point, we were looking at a devastatingly bad security vulnerability. Right up there in the upper echelons of epic security failures. All we knew is that our extremely popular data validation library fails to block harmful data, and that this data is trivial to sneak through. All you need to do is add __proto__ and some crap to a JSON input and send it on its way to an application built using our tools.

(戏剧性的停顿)

¥(Dramatic pause)

我们知道我们必须修复 joi 以防止这种情况发生,但考虑到这个问题的规模,我们必须以一种不会引起太多关注的方式来修复它 - 不会让它太容易被利用 - 至少在大多数系统收到更新的几天内。

¥We knew we had to fix joi to prevent this but given the scale of this issue, we had to do it in a way that will put a fix out without drawing too much attention to it — without making it too easy to exploit — at least for a few days until most systems received the update.

偷偷修复并不是最难完成的事情。如果将其与无目的的代码重构结合起来,并添加一些不相关的错误修复,也许还有一个很酷的新功能,那么你可以发布新版本,而无需引起人们对正在修复的实际问题的关注。

¥Sneaking a fix isn't the hardest thing to accomplish. If you combine it with an otherwise purposeless refactor of the code, and throw in a few unrelated bug fixes and maybe a cool new feature, you can publish a new version without drawing attention to the real issue being fixed.

问题是,正确的修复会破坏有效的用例。你看,joi 无法知道你是否希望它忽略你设置的原型,或者阻止攻击者设置的原型。修复漏洞的解决方案会破坏代码,而破坏代码往往会引起很多关注。

¥The problem was, the right fix was going to break valid use cases. You see, joi has no way of knowing if you want it to ignore the prototype you set, or block the prototype set by an attacker. A solution that fixes the exploit will break code and breaking code tends to get a lot of attention.

另一方面,如果我们发布了适当的(语义版本化)修复程序,将其标记为重大更改,并添加新的 API 以明确告诉 joi 你希望它对原型执行什么操作,我们将与全世界分享如何利用此漏洞,同时也使系统升级更加耗时(构建工具永远不会自动应用重大更改)。

¥On the other hand, if we released a proper (semantically versioned) fix, mark it as a breaking change, and add a new API to explicitly tell joi what you want it to do with the prototype, we will share with the world how to exploit this vulnerability while also making it more time consuming for systems to upgrade (breaking changes never get applied automatically by build tools).

绕道而行

¥A detour

虽然当前的问题与传入请求有效负载有关,但我们必须暂停并检查它是否也会影响通过查询字符串、cookie 和标头传入的数据。基本上,任何从文本序列化为对象的内容。

¥While the issue at hand was about incoming request payloads, we had to pause and check if it could also impact data coming via the query string, cookies, and headers. Basically, anything that gets serialized into objects from text.

我们很快确认节点默认查询字符串解析器及其标头解析器都很好。我发现了 base64 编码的 JSON cookie 以及自定义查询字符串解析器的使用的一个潜在问题。我们还编写了一些测试来确认最流行的第三方查询字符串解析器 - qs - 不存在漏洞(它不存在!)。

¥We quickly confirmed node default query string parser was fine as well as its header parser. I identified one potential issue with base64-encoded JSON cookies as well as the usage of custom query string parsers. We also wrote some tests to confirm that the most popular third-party query string parser  — qs —  was not vulnerable (it is not!).

开发

¥A development

在整个分类过程中,我们只是假设带有中毒原型的违规输入是从 hapi(连接 hapi.js 生态系统的 Web 框架)进入 joi 的。Lob 团队进一步调查发现问题更加微妙。

¥Throughout this triage, we just assumed that the offending input with its poisoned prototype was coming into joi from hapi, the web framework connecting the hapi.js ecosystem. Further investigation by the Lob team found that the problem was a bit more nuanced.

hapi 使用 JSON.parse() 来处理传入数据。它首先将结果对象设置为传入请求的 payload 属性,然后将该同一对象传递给 joi 进行验证,然后再传递给应用业务逻辑进行处理。由于 JSON.parse() 实际上并未泄漏 __proto__ 属性,因此它将使用无效密钥到达 joi 并验证失败。

¥hapi used JSON.parse() to process incoming data. It first set the result object as a payload property of the incoming request, and then passed that same object for validation by joi before being passed to the application business logic for processing. Since JSON.parse() doesn't actually leak the __proto__ property, it would arrive to joi with an invalid key and fail validation.

但是,hapi 提供了两个扩展点,可以在验证之前检查(和处理)有效负载数据。大多数开发者都已正确记录并很好地理解了这些内容。扩展点允许你在出于合法(通常是安全相关)原因进行验证之前与原始输入进行交互。

¥However, hapi provides two extension points where the payload data can be inspected (and processed) prior to validation. It is all properly documented and well understood by most developers. The extension points are there to allow you to interact with the raw inputs prior to validation for legitimate (and often security related) reasons.

如果在这两个扩展点之一期间,开发者在有效负载上使用 Object.assign() 或类似方法,则 __proto__ 属性将泄漏并成为实际原型。

¥If during one of these two extension points, a developer used Object.assign() or a similar method on the payload, the __proto__ property would leak and become an actual prototype.

松了一口气

¥Sigh of relief

我们现在面临的是一种截然不同的可怕程度。在验证之前操作有效负载对象并不常见,这意味着这不再是世界末日的场景。这仍然是潜在的灾难性的,但每个 joi 用户对一些非常具体的实现的暴露程度有所下降。

¥We were now dealing with a much different level of awfulness. Manipulating the payload object prior to validation is not common which meant this was no longer a doomsday scenario. It was still potentially catastrophic but the exposure dropped from every joi user to some very specific implementations.

我们不再关注秘密的 joi 版本。joi 中的问题仍然存在,但我们现在可以通过新的 API 并在接下来的几周内发布重大版本来正确解决它。

¥We were no longer looking at a secretive joi release. The issue in joi is still there, but we can now address it properly with a new API and breaking release over the next few weeks.

我们还知道,我们可以轻松地在框架级别缓解此漏洞,因为它知道哪些数据来自外部,哪些数据是内部生成的。该框架确实是唯一可以保护开发者免于犯此类意外错误的部分。

¥We also knew that we can easily mitigate this vulnerability at the framework level since it knows which data is coming from the outside and which is internally generated. The framework is really the only piece that can protect developers against making such unexpected mistakes.

好消息、坏消息还是没有消息?

¥Good news, bad news, no news?

好消息是这不是我们的错。这不是 hapi 或 joi 中的错误。这只有通过复杂的动作组合才能实现,而这并不是 hapi 或 joi 所独有的。其他所有 JavaScript 框架都可能发生这种情况。如果 hapi 坏了,那么世界就坏了。

¥The good news was that this wasn't our fault. It wasn't a bug in hapi or joi. It was only possible through a complex combination of actions that was not unique to hapi or joi. This can happen with every other JavaScript framework. If hapi is broken, then the world is broken.

太好了 - 我们解决了指责游戏。

¥Great — we solved the blame game.

坏消息是,当没有什么可指责的(除了 JavaScript 本身)时,修复它就困难得多。

¥The bad news is that when there is nothing to blame (other than JavaScript itself), it is much harder getting it fixed.

一旦发现安全问题,人们问的第一个问题是是否会发布 CVE。CVE — 常见漏洞和暴露 — 是已知安全问题的 database。它是网络安全的重要组成部分。发布 CVE 的好处是它会立即触发警报并发出通知,并且经常会破坏自动构建,直到问题得到解决。

¥The first question people ask once a security issue is found is if there is going to be a CVE published. A CVE — Common Vulnerabilities and Exposures — is a database of known security issues. It is a critical component of web security. The benefit of publishing a CVE is that it immediately triggers alarms and informs and often breaks automated builds until the issue is resolved.

但我们把它固定到什么地方呢?

¥But what do we pin this to?

也许,什么也没有。我们仍在争论是否应该给某些版本的 hapi 加上警告标签。"we" 是节点安全流程。由于我们现在有了新版本的 hapi,默认情况下可以缓解该问题,因此可以将其视为修复。但由于该修复并不是针对 hapi 本身的问题,因此声明旧版本有害并不完全正确。

¥Probably, nothing. We are still debating whether we should tag some versions of hapi with a warning. The "we" is the node security process. Since we now have a new version of hapi that mitigate the problem by default, it can be considered a fix. But because the fix isn't to a problem in hapi itself, it is not exactly kosher to declare older versions harmful.

发布有关 hapi 早期版本的咨询,其唯一目的是促使人们认识并升级,这是对咨询流程的滥用。我个人同意滥用它来提高安全性,但这不是我的决定。截至撰写本文时,该问题仍在争论中。

¥Publishing an advisory on previous versions of hapi for the sole purpose of nudging people into awareness and upgrade is an abuse of the advisory process. I'm personally fine with abusing it for the purpose of improving security but that's not my call. As of this writing, it is still being debated.

解决方案业务

¥The solution business

缓解这个问题并不难。使其规模化和安全化需要更多的投入。由于我们知道有害数据可以从哪里进入系统,并且我们知道在哪里使用了有问题的 JSON.parse(),因此我们可以用安全的实现替换它。

¥Mitigating the issue wasn't hard. Making it scale and safe was a bit more involved. Since we knew where harmful data can enter the system, and we knew where we used the problematic JSON.parse() we could replace it with a safe implementation.

一个问题。验证数据的成本可能很高,我们现在计划验证每个传入的 JSON 文本。内置 JSON.parse() 实现速度很快。真的真的很快。我们不太可能建立一个更安全、更快速的替代品。尤其是不是一夜之间并且不引入新的错误。

¥One problem. Validating data can be costly and we are now planning on validating every incoming JSON text. The built-in JSON.parse() implementation is fast. Really really fast. It is unlikely we can build a replacement that will be more secure and anywhere as fast. Especially not overnight and without introducing new bugs.

很明显,我们将用一些额外的逻辑封装现有的 JSON.parse() 方法。我们只需确保它不会增加太多开销。这不仅是出于性能考虑,也是出于安全考虑。如果我们通过简单地发送特定数据来轻松减慢系统速度,那么我们就可以以非常低的成本轻松执行 拒绝服务攻击

¥It was obvious we were going to wrap the existing JSON.parse() method with some additional logic. We just had to make sure it was not adding too much overhead. This isn't just a performance consideration but also a security one. If we make it easy to slow down a system by simply sending specific data, we make it easy to execute a DoS attack at very low cost.

我想出了一个极其简单的解决方案:首先使用现有工具解析文本。如果这没有失败,请扫描原始文本以查找有问题的字符串 "proto"。仅当我们找到它时,才对该对象执行实际扫描。我们无法阻止对 "proto" 的每次引用 - 有时它是完全有效的值(例如在这里写关于它的内容并将此文本发送到 Medium 进行发布时)。

¥I came up with a stupidly simple solution: first parse the text using the existing tools. If this didn't fail, scan the original raw text for the offending string "proto". Only if we find it, perform an actual scan of the object. We can't block every reference to "proto" — sometimes it is perfectly valid value (like when writing about it here and sending this text over to Medium for publication).

这使得 "成功路径" 几乎和以前一样快。它只是添加了一个函数调用、一个快速文本扫描(同样,非常快的内置实现)和一个条件返回。该解决方案对预计通过它的绝大多数数据的影响可以忽略不计。

¥This made the "happy path" practically as fast as before. It just added one function call, a quick text scan (again, very fast built-in implementation), and a conditional return. The solution had negligible impact on the vast majority of data expected to pass through it.

下一个问题。原型属性不必位于传入对象的顶层。它可以嵌套在内部深处。这意味着我们不能仅仅检查它是否存在于顶层。我们需要递归地迭代该对象。

¥Next problem. The prototype property doesn't have to be at the top level of the incoming object. It can be nested deep inside. This means we cannot just check for the presence of it at the top level. We need to recursively iterate through the object.

虽然递归函数是最受欢迎的工具,但在编写安全意识代码时它们可能会带来灾难性的后果。你会看到,递归函数增加了运行时调用堆栈的大小。循环次数越多,调用堆栈就越长。在某个时候 - KABOOM - 你达到最大长度并且进程终止。

¥While recursive functions are a favorite tool, they could be disastrous when writing security-conscious code. You see, recursive function increase the size of the runtime call stack. The more times you loop, the longer the call stack gets. At some point — KABOOM— you reach the maximum length and the process dies.

如果无法保证传入数据的形状,递归迭代就会成为公开的威胁。攻击者只需要制作一个足够深的对象即可使你的服务器崩溃。

¥If you cannot guarantee the shape of the incoming data, recursive iteration becomes an open threat. An attacker only needs to craft a deep enough object to crash your servers.

我使用了扁平循环实现,它不仅内存效率更高(函数调用更少,临时参数传递更少)而且更安全。我指出这一点并不是为了吹牛,而是为了强调基本的工程实践如何创建(或避免)安全陷阱。

¥I used a flat loop implementation that is both more memory efficient (less function calls, less passing of temporary arguments) and more secure. I am not pointing this out to brag, but to highlight how basic engineering practices can create (or avoid) security pitfalls.

进行测试

¥Putting it to the test

我将代码发送给两个人。首先到 内森·拉弗尼埃 仔细检查解决方案的安全属性,然后到 马泰奥·科里纳 查看性能。他们在各自字段都是最优秀的,而且常常是我的首选人员。

¥I sent the code to two people. First to Nathan LaFreniere to double check the security properties of the solution, and then to Matteo Collina to review the performance. They are among the very best at what they do and often my go-to people.

性能基准测试证实 "成功路径" 实际上不受影响。有趣的发现是,删除有问题的值比抛出异常更快。这提出了一个问题,即新模块(我称之为 bourne)的默认行为应该是什么 - 错误或清理。

¥The performance benchmarks confirmed that the "happy path" was practically unaffected. The interesting findings was that removing the offending values was faster then throwing an exception. This raised the question of what should be the default behavior of the new module — which I called bourne —  error or sanitize.

同样,我们担心的是应用会遭受 DoS 攻击。如果使用 __proto__ 发送请求会使速度变慢 500%,这可能是一个容易被利用的载体。但经过更多测试后,我们确认发送任何无效的 JSON 文本都会产生非常相似的成本。

¥The concern, again, was exposing the application to a DoS attack. If sending a request with __proto__ makes things 500% slower, that could be an easy vector to exploit. But after a bit more testing we confirmed that sending any invalid JSON text was creating a very similar cost.

换句话说,如果你解析 JSON,无效值将会花费更多成本,无论是什么导致它们无效。同样重要的是要记住,虽然基准测试显示扫描可疑对象的成本百分比很高,但 CPU 时间的实际成本仍然只有几毫秒。值得注意和测量很重要,但实际上并没有害处。

¥In other words, if you parse JSON, invalid values are going to cost you more, regardless of what makes them invalid. It is also important to remember that while the benchmark showed the significant % cost of scanning suspected objects, the actual cost in CPU time was still in the fraction of milliseconds. Important to note and measure but not actually harmful.

永远的哈比

¥hapi ever-after

有很多事情值得感恩。

¥There are a bunch of things to be grateful for.

Lob 团队最初的披露是完美的。这是私下向正确的人报告的,并提供了正确的信息。他们跟进了更多调查结果,并给了我们时间和空间以正确的方式解决问题。多年来,Lob 也是我 hapi 工作的主要赞助商,财政支持对于让其他一切发生至关重要。稍后会详细介绍。

¥The initial disclosure by the Lob team was perfect. It was reported privately, to the right people, with the right information. They followed up with additional findings, and gave us the time and space to resolve it the right way. Lob also was a major sponsor of my work on hapi over the years and that financial support is critical to allow everything else to happen. More on that in a bit.

分诊工作压力很大,但配备了合适的人员。拥有像 尼古拉斯·莫雷尔、Nathan 和 Matteo 这样愿意提供帮助的人至关重要。如果没有压力,这并不容易处理,但有了压力,如果没有适当的团队协作,很可能会出现错误。

¥Triage was stressful but staffed with the right people. Having folks like Nicolas Morel, Nathan, and Matteo, available and eager to help is critical. This isn't easy to deal with without the pressure, but with it, mistakes are likely without proper team collaboration.

我们很幸运地发现了实际的漏洞。一开始看起来像是灾难性的问题,最终变成了一个微妙但需要解决的直接问题。

¥We got lucky with the actual vulnerability. What started up looking like a catastrophic problem, ended up being a delicate but straight-forward problem to address.

我们也很幸运,拥有完全访问权限,可以在源头缓解它 - 不需要向某个未知的框架维护者发送电子邮件,并希望得到快速答复。hapi 对所有依赖的完全控制再次证明了它的有用性和安全性。不使用 hapi也许你应该

¥We also got lucky by having full access to mitigate it at the source — didn't need to send emails to some unknown framework maintainer and hope for a quick answer. hapi's total control over all of its dependencies proved its usefulness and security again. Not using hapi? Maybe you should.

从此以后幸福快乐

¥The after in happy ever-after

这就是我要利用这次事件重申可持续和安全开源的成本和需求的地方。

¥This is where I have to take advantage of this incident to reiterate the cost and need for sustainable and secure open source.

我一个人在这一问题上的时间就超过了 20 个小时。这就是半个工作周。它是在月底发布的,当时我已经花了 30 多个小时发布了 hapi 的新主要版本(大部分工作是在 12 月完成的)。这让我这个月的个人经济损失超过 5000 美元(我不得不减少有偿客户工作来腾出时间)。

¥My time alone on this one issue exceeded 20 hours. That's half a working week. It came at the end of a month were I already spent over 30 hours publishing a new major release of hapi (most of the work was done in December). This puts me at a personal financial loss of over $5000 this month (I had to cut back on paid client work to make time for it).

如果你依赖我维护的代码,这正是你想要的支持、质量和 promise 级别(让我们诚实地说 - 期待)。你们中的大多数人都认为这是理所当然的 - 不仅仅是我的工作,还有数百名其他专门的开源维护者的工作。

¥If you rely on code I maintain, this is exactly the level of support, quality, and commitment you want (and lets be honest — expect). Most of you take it for granted — not just my work but the work of hundreds of other dedicated open source maintainers.

因为这项工作很重要,所以我决定尝试使其不仅在财务上可持续,而且还要发展和扩大。还有很多需要改进的地方。这正是促使我实现 3 月份即将推出的新 商业许可计划 的原因。你可以阅读有关 此处 的更多信息。

¥Because this work is important, I decided to try and make it not just financially sustainable but to grow and expand it. There is so much to improve. This is exactly what motivates me to implement the new commercial licensing plan coming in March. You can read more about it here.