personal_asset

你随手点的"记住密码",很可能没加密,就摆在那儿

"记住密码"很可能是明文存在浏览器里,自动续期机制也可能把自己等死。

你随手点的"记住密码",很可能没加密,就摆在那儿

最近帮一个项目做安全审查,翻到登录页那个"记住密码"的小勾选框,我心里没当回事——这功能太常见了,几乎每个网站的登录框下面都挂着它,勾上它,下次打开就不用再敲一遍用户名密码。谁会觉得这种东西能出什么大问题。

直到顺着代码往下翻,才发现自己想简单了。

"记住密码"要实现,浏览器得找个地方把账号信息存下来,下次打开页面时再读出来自动填上。这个"存的地方",很多网站用的是浏览器提供的 localStorage——你可以把它理解成浏览器给每个网站分配的一个小抽屉,网站的代码随时能打开读、也能随时写进去,关掉浏览器再打开,抽屉里的东西还在。

这个项目的做法是:把用户名和密码原样塞进了这个抽屉,不加密,明文。也就是说,只要有一段代码能在这个网页上跑起来,就能直接打开抽屉,把密码原样读走。

能在网页上"跑起来的代码",听着像是要黑进服务器那种大动静,其实门槛没那么高。常见的三种情况:这个网站自己有个 XSS 漏洞(攻击者往页面里塞了一段能执行的脚本,中小型网站里并不罕见);你装的某个浏览器插件本身就在读所有网页内容(不少免费插件干的就是这个);或者你的电脑上被塞了别的脚本。这三种情况都不用碰你的账号密码本身,只要能在你浏览器里跑一行 JavaScript,就能把抽屉打开、把明文密码读走。

这和"记住登录状态"完全是两回事。正经做法是发一个有效期很短的令牌,令牌丢了顶多重新登录一次,代价是几秒钟的麻烦。密码不一样——密码是你在很多地方复用的东西,一旦被读走,对方拿着它不止能登这一个网站,代价是长期的、连锁的。这道题真正划分风险高低的,不是"存没存东西",是"存的这东西,丢了以后能造成多大范围的伤害"。

这个"记住密码"我后来直接删掉了,改成最多记用户名——密码这道题,答案就是别存。

同一次审查里,还有个更隐蔽的问题,跟密码没关系,跟"自动"两个字有关。

现在很多网站会做登录态的自动续期:令牌快过期了,系统在后台悄悄用另一个"刷新令牌"帮你换一个新的,你完全无感,不会被弹出来重新登录。这个项目也做了这套逻辑,而且做得挺完整——所有请求先经过一道"检查站"(专业叫拦截器),检查令牌过期没有,过期就自动去刷新。

问题出在检查站自己身上。当"刷新令牌"本身也失效了,系统去发起刷新请求——而这个刷新请求,也要经过同一道检查站。检查站一看,这个请求也没有有效登录态,于是又想再触发一次刷新……它在等一个自己发起、永远不会有结果的刷新。所有请求卡在检查站前面,不报错,就是转圈圈,用户看到的是页面一直"加载中",永远加载不完。

这两个坑看着不是一回事——一个是明文存密码,一个是逻辑死循环——但病根一样:设计一个"自动帮你处理"的机制时,只想清楚了正常情况怎么走,没想清楚这个机制自己失败那一刻该怎么办。方便功能越"聪明",越容易漏掉这一步,因为设计者的注意力全在"怎么让用户少操心"上,很少有人会想"如果这个帮用户省心的东西自己先出问题了怎么办"。

这两个坑修起来都不难:密码那道题的答案是别存;死锁那道题的答案是把刷新请求单独摘出来,让它自己不能再触发刷新。真正难的从来不是怎么修,是有没有人在设计的时候多问一句:这份方便,出问题的时候由谁来扛。

下次在哪个网站的登录框上顺手点了"记住密码",可以打开浏览器开发者工具(F12),在存储那一栏翻一下 localStorage,看看里面是不是真躺着一串没加密的密码。多数正规大厂不会这么干,但很多中小网站,未必。

来源