在日志记录中保护用户隐私数据的七个最佳实践
2019年度“315”晚会人工智能拨打骚扰电话的情节,让大众了解到在信息时代,保护个人隐私的重要性。本篇文章分享了在日志记录中保护用户隐私数据的七个最佳实践。
与“中国人愿意用隐私交换便利性”的心态完全不同,欧美国家在个人隐私保护方面明显走得更早也更远一些。在2018年5月GDPR发布前后的一段时间里,保护个人隐私相关的需求被迅速提高了优先级,而像我这样一个开发国际化产品的普通程序员,日常工作也因此受到影响,我们放下手中的业务需求卡(Story),转而去做GDPR相关的安全需求。
从公司最高层面,自上而下,我们采取了一系列相关动作,比如梳理我们基础设施架构图、数据流图、数据格式……,而其中很大一部分就是保护日志中的个人信息。
一般在医疗保健或金融行业中,限制访问客户的敏感数据有着非常严格的规定,尤其欧洲GDPR颁布之后,公司泄露个人数据的后果也非常严重。如果没有适当的流程、工具或者意识,为了开发调试的方便,常常会无意地将隐私数据写入日志文件。
并没有一种一劳永逸的方式来避免个人信息出现在日志中,但我们可以通过下面的措施,并内建在自己平时的开发工作中,来尽量规避出现个人隐私安全问题。
一、确定什么是隐私数据
在我们深入讨论怎样避免个人隐私数据出现在日志之前,我们来界定什么是隐私数据:
- 个人可标识数据(PII):如社会安全号码,数据组合(如名字+出生日期或姓氏+邮政编码)或用户生成的数据(如电子邮件或用户名,如BillGates@hotmail.com),手机号。
- 健康信息
- 财务数据(如信用卡号)
- 密码
- IP地址:IP地址也有可能是个人隐私数据,尤其是与个人可标识数据与其有某种绑定关系。(而2019年的3.15晚会介绍一种将MAC也变成了PII的方式)
个人隐私信息不一而足,重要的是要根据实际情况彻查应用内的数据,来确定什么是敏感的.
二、分离隐私数据
处理隐私数据时,应尽量减少系统使用这些数据。例如,在数据库表设计时,使用SSN或电子邮件地址作为人的主键的确很顺手,但是,如果这样做,系统在许多不同部分(数据库表,API端点等)都要处理和存储这些敏感字段,更好的方法是分离隐私数据,只在在必要时才使用它。
一种常见的解决方案是使用查找表用随机ID替换隐私字段。例如:
SSN | 外键
-------------------------
999-99-9999 |5a2_cXKrt32DcWOJpJlyhr7FhTcLPfvlEAb1eA2H
即使SSN是主键,主Person表外的表和服务也只使用外键。
三、避免在URL中出现个人隐私信息
如果您正在构建RESTful API并且您的用户数据是在电子邮件地址上键入的,则可能很容易拥有一个端点,如:/user/<email>
。请求URL通常由代理和Web服务器记录,因此如果您这样做,电子邮件必然会以日志结束。要将敏感数据保留在您的网址之外,您可以选择几个选项。
选项1.根据建议#1,不要将敏感字段用作唯一标识符。对于端点URL,请改用这些外部ID。
选项2.违反REST原则并将敏感值作为POST正文的一部分传递,即使它是只读请求。Web服务器通常不会记录POST请求的正文,因此您的敏感字段不会记录日志。
问题
在设计早期,您应确定系统中哪些数据被视为敏感数据。如果你还没有这样做,那么可能需要付出艰苦的努力才能在游戏后期进行类似的API设计更改。
四、对象打印重写toString方法
因此,您已经划分了代码(#1)并将数据保留在URL(#2)之外。但是,您的用户端点包含一些日志记录语句,以帮助调试服务。它可能看起来像:
logger.info("为用户$ {user}更新电子邮件);
代码库中的某个位置是将a序列化为user
字符串的方法。也许它在课堂定义中。在该定义中,请确保使用敏感数据编辑字段:
class UserAccount {
id:string
username:string
passwordHash:string
firstName:string
lastName:string
...
public toString(){
return "UserAccount (${this.id})";
}
记录所有字段可能很诱人toString
,但事实证明你真的只需要id
。如果在调试过程中需要跟踪有关用户的更多详细信息,则可以在完成后查找它们id
。
问题
这不会阻止开发人员直接记录字段,例如: logger.info("The user's details are: ${user.firstName} ${user.lastName}");
五、结构化对象打印时屏蔽隐私字段
通过基于字符串的API记录,console.log()
或者printf
两者都需要将数据转换为字符串,现在通常被认为是一种反模式。将数据吐出来进行调试可能很容易,但解析这些数据很痛苦,而且可能缺少有用的上下文。使用结构化日志记录而不是字符串,可以记录键/值或嵌套对象。有关当前上下文的某些详细信息(例如,请求ID或服务器主机名)可以自动注入请求中。
如果你不熟悉结构化日志,那么honeycomb.io博客就有一个很好的介绍:你可以发明结构化日志。
结构化日志记录后,您现在可以将某些属性列入黑名单,以便在运行时对其进行过滤。例如:
Blacklist = ["firstName", "lastName", "SSN"]
SSNRegex = r"^\d{3}-?\d{2}-?\d{4}$"
EmailRegex = r".+@.+";
class Logger {
log(details: Map<string,string>) {
const cleanedDetails = details.map( (key, value) => {
if (Blacklist.contains(key) ||
SSNRegex.match(value) ||
EmailRegex.match(value)) {
return (key, "<redacted>");
}
return (key, value);
}
console.log(JSON.stringify(cleanedDetails));
}
}
在上面的记录器中,我们有一个基于黑名单和两个正则表达式的简单启发式,以跳过看起来像SSN或电子邮件的值。
问题
这并不能阻止某人做类似的事情:
logger.log({'pleaseLogThis': user.firstName});
要么
logger.log('{firstName:Joe}');
也可能有一个误报,其中一个日志记录语句过滤掉它不应该的东西 - 但根据我的经验,这是非常罕见的,很容易在开发早期捕获。
五、通过代码审查尽量避免
作为代码审查的一部分,审阅者应查找包含敏感数据的日志记录。如果您使用的是Pull Request Template,则可能需要在模板中设置一个复选框,以便审阅者确认他们已在更改中验证了日志记录语句。
什么可能出错
根据我的经验,审稿人倾向于掩盖日志记录。它需要文化的转变来注意并密切关注它们。
六、通过QA和自动化测试
虽然您的QA团队应该测试系统中的许多流程是否正常工作,但他们的测试并不止于此。如果测试是自动化的并且使用可预测的数据,则测试可以自动检查该数据是否未在日志中结束。例如,如果Web表单包含名字,姓氏和SSN字段,则在运行Selenium套件之后,测试还应在应用服务器日志中查找该名字,姓氏和SSN。
问题
QA团队通常没有正确的访问权限,甚至不知道要检查哪些系统。如果他们一直在进行黑盒测试,那么需要一些工作才能让他们加快速度。
七、在日志系统中配置告警
与#4和#6类似,您可以在日志记录系统中编写测试以查找某些数据模式。例如,查找SSN或搜索常见测试数据的正则表达式。此测试应该在您的staging或dev环境中就位,以便在将代码提升为生产之前捕获它。
虽然这看起来有点矫枉过正,但我已经看到这种技术在它投入生产之前会发现许多可能的PII漏洞。如果您有一个复杂的系统,系统某个部分的微小变化可能会产生意想不到的后果,这些后果很难以其他方式捕获。
问题
您的登台环境的应用程序配置(例如日志级别)是否与生产相匹配?您是否将登录的日志记录到与生产相同的日志记录系统中?
有时,警报可能过于嘈杂,因为将为DEBUG级别的日志记录配置分段,这会导致更多消息。有时,团队会忽略分段提醒。根据我的经验,以与生产中相同的活力处理分段中的警报或中断非常重要。否则,你可能不会提前抓住这些东西。
总结
这些最佳实践可以帮助您将敏感数据保留在日志之外。它肯定不是一个完整的设置,可以让你为HIPAA或SOC2审计做好准备,但它们可以使你的创业公司在这方面有一个良好的立足点。即使您不在高度合规行业,您也应该认真对待保持客户数据的机密性。
但在一天结束时,清单不会使您的系统安全。您的公司和软件团队需要投资建立安全系统的文化。
如果您想了解有关日志记录和安全性的更多信息,OWASP拥有许多优秀的资源,包括Logging Cheat Sheet。
我看到数据潜入日志的其他方式包括:
- 数据库日志 - 如果配置错误,他们最终可能会写出查询和查询参数
- 其他错误配置 - 我发现部署错误无意中将日志更改为生产中的DEBUG数十次。虽然您的日志记录可能没问题,但ORM或其他第三方库可能会丢弃大量信息。
- 客户端数据泄露 - 您需要注意如何在客户端存储数据,就像Healthcare.gov在2015年出名一样。