作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Hrvoje Gazibara的头像

Hrvoje Gazibara

Hrvoje是一名软件工程师,拥有5年以上使用PHP等语言的经验, Python, JavaScript, SQL, and HTML.

Previously At

AVL
Share

在过去十年中,Web应用程序的复杂性呈爆炸式增长. 它们已经从用于联系表单和投票的简单容器演变为成熟的应用程序. 我们可以将它们与大型桌面应用程序在大小和性能上进行比较. 随着复杂性的急剧上升和功能丰富的应用程序数量的增加, 投入大量时间和精力使所有应用程序组件尽可能安全已成为一种必要. 互联网用户的大量增加使得解决数据和应用程序用户的保护问题变得更加重要. 有大量的威胁试图潜入其中,给所有相关人员带来严重的头痛.

2001年,一个新的组织进入了舞台. 它的目标是对抗影响网站和应用程序的安全问题. 它被恰当地命名为开放Web应用程序安全项目(OWASP). Nowadays, 它发布资源, 组织会议, 并提出了web和应用程序安全标准. 事实上的标准 Web应用程序安全性 OWASP十大项目是什么. 它列出了十大最普遍的安全威胁. 决定什么可以进入的影响因素是大量的数据和社区反馈. 2017年底,该项目进行了更新. 对许多现代web应用程序至关重要的几个新问题得到了重视, 而有些人从名单上逃了出来.

本文补充了原始列表,并说明了列表的最新更改. 它描述了威胁, 试图提供清晰的例子,以便更容易理解, 并提出了应对安全威胁的方法.

从OWASP十大问题列表中删除

在2017年更新之前,2013年的榜单是最新的. 考虑到web应用程序现在构建和使用方式的变化, 只有彻底修改才有意义. 微服务正在分一杯羹, 新的酷炫框架正在取代普通的代码战斗装备. 这些事实意味着之前列出的一些威胁被移除,取而代之的是一些新的威胁.

在本文中,我们将确保刷新我们对长期被遗忘的问题的记忆, 同时也介绍了新的恶狼. 了解历史是避免重蹈覆辙的唯一可靠方法.

跨站点请求伪造

跨站点请求伪造(CSRF)是项目最近迭代中的“失败者”之一. 它消失了,因为许多现代web框架都包含CSRF防御机制. 因此,将应用程序暴露于威胁的可能性会迅速降低.

不管CSRF是否退出列表,刷新一下我们的记忆还是有好处的. 让我们确保它不会比以往任何时候都更加强大.

从本质上讲,CSRF是一个让人感觉像是烟雾弹的漏洞. 攻击者欺骗不知情的用户在web应用程序中执行不想要的请求或操作. Simply put, 攻击者强迫受害者向第三方应用程序发送请求, 而受害者对请求的发送毫不知情. 该请求可以是检索资源的HTTP GET请求, or even worse, 改变受害者控制下的资源的HTTP POST请求. During the attack, 受害者认为一切都很好, 大多数情况下,他们甚至没有注意到后台正在发生的事情. 空气散去后,损害已经造成,或者有什么东西不见了,没有人知道发生了什么.

目标应用程序中成功的先前用户身份验证是使陷阱有效的原因. 该用户在攻击前的某个时刻已登录到应用程序. 应用程序向受害者发送了一个cookie来记住他们. 一旦web浏览器发送恶意请求, cookie会自动与任何潜在的有效负载一起发送,并且应用程序不会反对将请求提供给它已经知道的用户.

最著名的例子之一是欺骗用户将资金从他们的帐户转移到攻击者控制的帐户. 用户登录到一个电子银行系统来检查他们的账户余额. 之后,他们访问一个在线论坛,查看对新手机的评论. An attacker, 用炸药钓鱼, 发布一个评论,其中包括一个看似破碎的图像超链接的图像. 而不是一个真正的超链接, 攻击者使用电子银行系统内部使用的超链接将资金从帐户a转移到帐户B: http://payments.dummybank.com?receiver=attacker&amount=100. 银行系统将经过身份验证的用户作为发送方,并将“receiver”参数的值作为资金的接收方. 当然,攻击者指定他们的离岸账户作为接收者.

因为浏览器在渲染页面时会自动加载图像, 请求在后台发生. 如果银行的支付系统使用HTTP GET请求实现转账, 没有什么能阻止灾难的发生. 注意,这个例子很简单,而且传输很可能不是通过HTTP GET处理的. However, the attacker can, 只是稍微困难一点, 设法更改论坛的HTML消息发布表单中的属性" action ". 然后浏览器将请求发送到银行的支付系统,而不是论坛的后端.

偷钱只是众多例子中的一个. 更改用户的电子邮件地址或进行意外购买也属于这一类. As it often happens, 社会工程和一些技术知识是对抗软件工程错误的有效杠杆.

在设计系统时,请牢记以下几点:

  • Do not 使用HTTP GET请求封装修改资源的操作. 您应该仅将这些请求用于检索信息. 记住,经验法则是使GET请求幂等.
  • Do, 在内部使用HTTP POST请求传输数据时, 倾向于以JSON格式发送数据, XML或其他格式,而不是将参数编码为查询字符串. 使用重要的数据格式可以减少有人创建伪造的HTML表单将数据发送到您的服务的危险.
  • Do 确保在HTML表单中创建并包含一个唯一且不可预测的令牌. 这些令牌对于每个请求也应该是唯一的. 检查这些令牌的存在和正确性将降低发生威胁的风险. 找出令牌,然后用它来伪造请求, 攻击者需要访问您的系统并直接从那里获取令牌. 由于令牌是一次性的,因此它们不能在恶意代码中重用它们.

当与跨站点脚本(XSS)结合使用时,此漏洞的影响甚至更糟。. 如果攻击者可以将恶意代码注入喜爱的网站或应用程序, 攻击的范围变得更加重大和危险. Even more critical, 如果可能发生XSS攻击,攻击者可以绕过一些针对CSRF的保护机制.

请记住,CSRF并没有消失,只是不像以前那么常见了.

CSRF运行示意图-从OWASP前10名中删除

未经验证的重定向和转发

有很多应用, 完成一个动作后, 重定向或转发用户到同一部分的另一部分, or even some other, application. For example, 成功登录到应用程序将触发重定向到主页或用户最初访问的页面. 通常,目的地是表单操作地址或链接地址的一部分. 如果处理重定向或转发的组件不能确保目标地址确实是应用程序生成的地址, 潜在的威胁出现了. 这是一个称为“未经验证的重定向和转发”的安全漏洞.”

未经验证的重定向和转发被认为是危险的两个主要原因是网络钓鱼和凭据劫持. 攻击者可以设法改变重定向/转发目标位置,并将用户发送到与原始应用程序几乎没有区别的恶意应用程序. 毫无戒心的用户将其凭据和机密信息泄露给恶意的第三方. 在他们意识到发生了什么之前,已经太晚了.

As an example, Web应用程序经常实现支持重定向到最后访问页面的登录. 为了方便地做到这一点,HTML表单的action属性可能看起来像这样 http://myapp.example.com/signin?url=http://myapp.example.com/puppies. 你是一个超级喜欢小狗的人, 所以安装一个浏览器扩展是有意义的,它可以用你最喜欢的小狗的缩略图取代网站的favicons. 不幸的是,这是一个狗咬狗的世界. 浏览器扩展的作者决定利用你无条件的爱,并引入一个额外的“功能”.“每次你访问你最喜欢的小狗粉丝网站, 它将表单的action属性中的“url”参数替换为指向自己站点的链接. 因为网站看起来一模一样, 当你给你的信用卡细节买小狗打牌, 你实际上是在资助一个恶意攻击者, not your hobby.

解决这个漏洞需要检查目标位置,确保它是预定的位置. 如果框架或库完成了完整的重定向或转发逻辑, 检查实现并在必要时更新代码是有益的. 否则,需要手工检查,防止攻击.

你可以开几种类型的支票. 为了最好的保护, 使用几种方法的组合,而不是只坚持其中一种.

  • 验证传出URL,确保它指向您控制的域.
  • 而不是使用显式地址, 在前端对它们进行编码,然后在后端对它们进行解码和验证.
  • 准备可信url的白名单. 只允许转发和重定向到白名单位置. 与维护黑名单相比,更倾向于使用这种方法. 黑名单通常只有在发生不好的事情时才会添加新项目. 白名单的限制更大.
  • 采用LinkedIn和其他一些应用程序使用的方法:向用户展示一个页面,要求他们确认重定向/转发, 明确表示他们将离开你的申请.

Merged Issues

列表上的大多数问题都可以被标记为实现中的缺陷, 要么是因为缺乏知识,要么是因为对潜在威胁的调查不够深入. 这两个原因都可以归因于缺乏经验,未来考虑到这些问题的解决方案很容易——只要确保学得更多、更彻底. 一个特别棘手的问题依赖于做出太多假设的危险(但非常人性化)特征,以及开发和维护复杂计算机系统的困难. 属于这一类的一个漏洞是“访问控制中断”.”

中断访问控制

脆弱是由不充分造成的, 或者完全没有, 对应用程序的某些部分进行授权和访问控制. 在OWASP十大项目的前几次迭代中, 有两个问题:不安全的直接对象引用和缺少函数级访问控制. 由于它们的相似性,它们现在合并为一个.

直接对象引用

在url中经常使用直接对象引用来标识要操作的资源. For example, 当用户登录时, 他们可以通过点击包含他们的个人资料标识符的链接来访问他们的个人资料. 如果相同的标识符存储在数据库中并用于检索概要信息, 该应用程序假定人们只能通过登录页面进入个人资料页面, 更改URL中的概要标识符会暴露另一个用户的概要信息.

一个设置删除配置文件表单URL的应用程序 http://myapp.example.com/users/15/delete 明确表示该应用程序中至少有14个其他用户. 在这种情况下,弄清楚如何获取其他用户的删除表单并不是什么难事.

这个问题的解决方案是对每个资源执行授权检查,而不假设只有某些路径可以到达应用程序的某些部分. In addition, 删除直接引用并使用间接引用是向前迈出的另一步,因为这使得恶意用户很难弄清楚如何创建引用.

在开发过程中,作为预防措施,写下一个简单的状态机图. 让这些状态代表应用程序中的不同页面,以及用户可以采取的动作的转换. 这使得它更容易列出所有需要特别注意的过渡和页面.

状态机图的说明

缺少功能级访问控制

缺少函数级访问控制与不安全的直接对象引用非常相似. In this case, 用户修改URL或其他参数来尝试访问应用程序中受保护的部分. 如果没有适当的访问控制检查访问级别, 用户可以获得对应用程序资源的特权访问,或者至少了解它们的存在.

从例子中借用直接对象引用, 如果应用程序假设访问删除表单的用户是超级用户,因为超级用户可以看到到删除表单的链接, 无需进一步授权, 一个巨大的安全漏洞出现了. 依靠某些UI元素的可用性并不是适当的访问控制.

通过始终确保在应用程序的所有层执行检查,可以解决这个问题. 前端接口可能不是恶意用户访问您的域层的唯一途径. 此外,不要依赖用户传递的有关其访问级别的信息. 执行适当的会话控制,并始终仔细检查接收到的数据. 仅仅因为请求体说用户是管理员,这并不意味着他们真的是管理员.

访问控制中断现在结合了与访问控制不足相关的所有问题, 无论是在应用程序级别还是在系统级别, 比如文件系统配置错误.

破坏访问控制的示意图

OWASP十大新问题列表

新的前端框架的出现和新的软件开发实践的采用将安全问题转移到了全新的主题上. 新技术还设法解决了我们以前手工处理的一些常见问题. 因此,修订名单并根据现代趋势进行调整是有益的.

XML外部实体

XML标准提供了一个鲜为人知的概念,称为外部实体, 它是文档数据类型定义(DTD)的一部分. 它允许文档作者指定指向外部实体的链接,然后这些实体可以被引用并包含在主文档中. 一个非常简单的例子是:



  
]>

  &bar;

在解析过程中,引用 &bar; 被定义实体的内容所替换,从而产生 baz.

如果应用程序接受外部输入并包含它, without any checks, 直接转换为XML文档定义, 大范围的数据泄露和攻击将成为可能.

神奇的是,实体不必是一个简单的字符串—它可以是对文件系统上文件的引用. XML解析器将乐于接受指定文件的内容,并将其包含到生成的响应中, 可能泄露敏感的系统信息. 如OWASP所示, 通过将实体定义为,很容易获得关于系统用户的信息

]>

此漏洞的一个特别麻烦的“特性”是容易执行拒绝服务攻击的可能性. 一种简单的方法是列出无尽文件的内容,例如 /dev/random. 另一种方法是创建一个实体序列,每个实体多次引用前一个实体. 这将最终引用转换为可能非常宽且非常深的树的根,其解析可能耗尽系统内存. 这次攻击甚至被称为“十亿笑”. 如维基百科所示, 定义了一系列虚拟实体, 使攻击者有机会在最终文档中包含10亿个lol.



 
 
 
 
 
 
 
 
 
 
]>
&lol9;

可以通过使用不那么复杂的数据格式来防止XML外部实体漏洞. JSON是一个很好的替代品, 提供一些预防措施,以及由于可能的攻击. 必须更新XML库,同时禁用外部实体处理和DTD. As always, 在使用来自不受信任来源的数据或将其包含在文档中之前,对其进行验证和消毒.

不安全的反序列化

When writing code, 开发人员有能力使用他们编写的代码来控制他们正在开发的系统. 如果能够控制第三方系统只编写少量甚至不编写代码的行为,该有多棒? 因为人不是完美的,图书馆也有缺陷, 这是完全可能的.

应用程序状态和配置通常是序列化和存储的. 如果序列化的数据与当前用户紧密耦合,有时浏览器充当存储引擎. 一个想要聪明一点并节省处理时间的应用程序可以使用cookie来标记用户已经登录. 因为cookie只能在登录成功后创建, 将用户名存储在cookie中是有意义的. 然后根据cookie的存在和内容对用户进行身份验证和授权. 如果人们没有恶意,就不会出错. 老实说,他们也不应该好奇.

如果一个好奇的用户在他们的机器上发现了一个cookie,他们会看到这样的内容:

{"username": "joe.Doe ", "expires": "2018-06-01 10:28:16"}

一个完全有效的序列化为JSON的Python字典,没有什么特别的. 好奇的用户可能会更改过期日期,以防止应用程序强制注销. 更好奇的用户可能会尝试将用户名修改为 "jane.doe". 如果这个用户名存在, 它将为现在可以访问私人数据的毫无戒心的用户打开一个全新的世界.

将数据序列化为JSON并保持所有内容透明的简单示例远远不是可能发生在您身上的最糟糕的事情. 如果攻击者实现了修改一些序列化数据, 他们可能会以某种方式修改它,迫使您的系统执行任意代码.

假设你正在构建一个REST API,它允许人们用Python编写自己的机器学习模型并将其上传到你的服务. 该服务将评估上传的模型并使用您的数据集进行训练. 这允许人们使用您的计算资源和大量可用数据集来快速轻松地构建模型.

该服务不以纯文本格式存储代码. Users pickle 他们的代码,使用他们的私钥加密,并将其发送到API进行培训. 当服务需要运行一个模型时,它会解密代码、解pickle并运行它. 棘手的部分是pickle协议是不安全的. 代码的构造方式可以允许在反序列化期间执行任意恶意代码.

Python的pickle协议允许类定义一个方法 __reduce__,它返回有关如何反序列化自定义对象的信息. 支持的返回值之一是由两个参数组成的元组:一个可调用对象和一个由要传递给可调用对象的参数组成的元组. 考虑到你的机器学习模型训练系统旨在提供代码结构的最大灵活性, 可以编写以下代码:

类算法(对象):
 def run(self):
   pass
 def __reduce__(自我):
   import itertools
   返回list, itertools.count(1), ))

一旦对象需要反序列化(取消pickle),一个函数 list 只带一个参数调用. The function list 是Python中的一个列表构造函数,而函数 itertools.count 从传递的形参开始,生成一个无限迭代器. 将无限迭代器转换为有限列表可能会对系统的性能和稳定性造成灾难性的后果.

解决这类漏洞的唯一方法是选择不对来自外部的数据进行反序列化. 如果这是不可能的, 建议使用校验和或数字签名来防止可能被恶意用户修改的数据反序列化. Also, 尝试设置一个与主系统分离的沙盒环境,以限制可能出现的问题的影响.

使用外部库对数据进行反序列化时, 例如XML或JSON, 尝试选择那些允许您在执行实际反序列化过程之前进行对象类型检查的类型. 这可能会捕获意外的对象类型,其唯一目的是损害您的系统.

与应用程序执行的所有其他操作一样,执行广泛的日志记录和监视. 反序列化经常发生或比正常情况更频繁地失败是正在发生不好的事情的信号. 及早发现问题.

记录和监控不足

您花了多少时间来确保记录应用程序中发生的所有警告和错误? 您是只存储代码中发生的错误,还是也记录验证错误? 当您的域的业务规则不满足时会发生什么? 如果不能在应用程序中持久化所有错误和可疑的活动,就会危及安全性和数据.

想象一下下面的场景. 与大多数应用程序一样,您的应用程序包含一个登录页面. 该表单有两个字段,一个用于输入电子邮件,另一个用于输入密码. 如果用户尝试登录并且他们提供了错误的密码,他们可以再试一次. Unfortunately, 错误尝试的次数没有限制, 所以登录页面不会在N次尝试失败后被锁定. 攻击者可以利用这个机会, 给定一个正确的电子邮件, 一直从彩虹表中输入密码,直到一个组合最终成功. 前提是您的应用程序足够安全,并且在将密码输入数据库之前对其进行了散列处理, 这种特殊的攻击是行不通的. 但是,您有识别入侵的机制吗?

就因为这一次没能打开你的登录页面, 这并不意味着其他人不会. 登录页面可能也不是你唯一的潜在后门. 如果不是因为其他原因,有人可能会试图使用破坏的访问控制来对付您. 即使是精心设计的应用程序也应该知道有人试图攻击它们, 即使这是不可能的. 但它总是这样.

要尽量保护自己免受这类攻击,请采取以下步骤:

  • 记录应用程序中发生的所有失败和警告, 无论是在代码中抛出的异常还是访问控制, validation, 数据操作错误. 所有存储的信息必须复制并保存足够长的时间,以便进行回顾性检查和分析.
  • 确定格式和持久层非常重要. Having a huge file with arbitrary text format is easy to accomplish; processing it later is not. 选择一种易于存储和读取数据的存储选项,以及一种允许轻松快速(反)序列化的格式. 将JSON存储在支持快速访问的数据库中简化了使用. 通过定期备份来保持数据库较小.
  • 如果您正在处理重要且有价值的数据, 保留可用于审计最终状态的操作的跟踪. 实现防止数据篡改的机制.
  • 让后台系统分析日志并在出现问题时提醒您. 检查(如果用户重复尝试访问应用程序的受保护部分,就像测试一样简单)有助于解决问题. 但是,不要用虚假的检查使系统过载. 监控系统必须作为单独的服务运行,不能影响主系统的性能.

在解决该问题时,要特别注意不要将错误日志泄露给外部用户. 如果不这样做,你的敏感信息就会暴露. 日志记录和监视应该可以帮助您解决问题, 而不是攻击者更有效率地工作.

日志记录和监控图

Next Steps

了解web应用程序中的潜在威胁和漏洞非常重要. 更重要的是开始在应用程序中识别它们并应用补丁来删除它们.

注意应用程序的安全性是软件开发项目所有步骤的重要组成部分. 软件架构师, developers, 测试人员必须将软件测试过程整合到他们的工作流程中. 在软件开发过程的适当步骤中利用安全性检查表和自动化测试来减少安全性风险是有益的.

无论您是在分析现有的应用程序还是开发一个新的应用程序, 你应该调查一下 OWASP应用安全验证标准项目 (ASVS). 该项目的目标是为应用程序安全性验证开发一个标准. 该标准列举了开发安全web应用程序的测试和要求. 测试分为1到3级, 1表示危险最小,3表示潜在威胁最大. 分类允许应用程序管理人员决定哪些威胁更有可能发生,也更重要. 没有必要在每个应用程序中包含每个测试.

新的和现有的web应用程序项目, 尤其是那些遵循敏捷原则的, 受益于保护其应用程序的结构化计划. 如果您决定使用,OWASP ASVS测试的计划会更容易 OWASP安全知识框架. 它是一个用于管理面向安全测试的sprint的应用程序, 还提供了一组关于如何解决常见安全问题的示例, 以及易于遵循的基于OWASP ASVS的清单.

如果您刚刚开始探索web应用程序安全性,并且需要一个安全的沙盒游乐场, 使用OWASP实现的web应用程序WebGoat. 这是一个故意不安全的web应用程序实现. 该应用程序指导您完成课程, 每节课都集中在一个安全威胁上.

在应用程序开发期间,请确保:

  • 识别威胁并确定优先级. 定义哪些威胁可能实际发生并对您的应用程序构成风险. 对威胁进行优先排序,并决定哪些威胁值得投入最多的开发和测试工作. 如果您提供的是静态博客,那么花费大量精力解决日志记录和监视不足的问题是没有多大意义的.
  • 评估应用程序的体系结构和设计. 在应用程序开发的后期阶段,有些漏洞很难解决. For example, 如果您打算执行第三方代码, 并且没有使用沙盒环境的计划, 防御不安全的反序列化和注入攻击是非常困难的.
  • 更新软件开发流程. 针对web应用程序威胁的测试必须尽可能是自动化的过程. 使用试图找到安全漏洞的自动化测试来扩展CI/CD工作流是有益的. 您甚至可以利用现有的单元测试系统来开发安全性测试并定期运行它们.
  • Learn and improve. 问题和漏洞列表不是静态的,也绝对不限于10或15个威胁. 新的功能和想法为新的攻击类型打开了大门. 阅读有关web应用程序安全领域的当前趋势以保持与时俱进是很重要的. Apply what you learn; otherwise, you are wasting your time.

Conclusion

Even though, 顾名思义, OWASP十大项目只列出了十个安全漏洞, 有成千上万的陷阱和后门威胁着你的应用程序, most importantly, 用户和他们的数据. 一定要时刻保持警惕,不断更新自己的知识,因为技术的变化和改进既有好处也有坏处.

哦,还有,别忘了,世界不是非黑即白的. Security vulnerabilities don’t come alone; they are often intertwined. 接触到一个通常意味着周围有一群人, 等着长出他们丑陋的脑袋, 即使这不是你的错, as the 系统安全开发人员,你仍然要修补漏洞来遏制网络犯罪. 使用示例请参见 被黑客入侵的信用卡号码仍然可以在谷歌上搜索到.

了解基本知识

  • OWASP代表什么?

    OWASP是开放Web应用程序安全项目的缩写. 它是一个在线社区,提供有关web应用程序安全的学习资源和工具.

  • OWASP前十名是什么?

    OWASP十大威胁是最常见的web应用程序安全威胁的列表, 由安全专家制作, 考虑社区反馈.

  • 什么是CSRF攻击?

    跨站点请求伪造是一种攻击,它使用户对其身份验证的应用程序执行不希望的请求.

  • 什么是未经验证的重定向和转发?

    未经验证的重定向和转发是一个安全漏洞,允许攻击者将用户重定向到不安全的web应用程序, 获得对受保护的应用程序资源的访问, 或者窃取特权用户信息, 所有这些都是通过注入攻击实现的.

  • 什么是注入攻击?

    注入攻击是一种允许攻击者将不需要的代码注入应用程序的攻击类型. 有几种类型的攻击, 基于应用, 其中最常见的是SQL注入和跨站脚本.

  • 什么是反序列化?

    反序列化是将字节流转换为加载到内存中的代码的过程. 原始字节流是由执行相反操作的序列化过程生成的.

  • 什么是攻击向量?

    攻击向量表示利用应用程序中的安全漏洞的方法. 注入无效的HTML img元素向银行的API资源发出请求,这是CSRF攻击中使用的攻击向量的一个示例.

  • XML中的实体是什么?

    XML实体是一种机制,用于定义XML文档或文档类型定义(DTD)中使用的项的别名。.

  • 什么是外部实体?

    外部实体是一种XML实体,使得文档作者很容易使用统一资源标识符(URI)将外部资源包含到文档中。.

  • 什么是OWASP WebGoat?

    OWASP WebGoat是一个故意不安全的web应用程序实现,它作为一种学习机制来教授web应用程序安全课程.

就这一主题咨询作者或专家.
Schedule a call
Hrvoje Gazibara的头像
Hrvoje Gazibara

Located in Zagreb, Croatia

Member since January 7, 2016

About the author

Hrvoje是一名软件工程师,拥有5年以上使用PHP等语言的经验, Python, JavaScript, SQL, and HTML.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

AVL

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.