在nodejs中如何防止ssrf攻击
什么是ssrf攻击
ssrf
(server-side request forgery
)是服务器端请求伪造,指攻击者能够从易受攻击的Web
应用程序发送精心设计的请求的对其他网站进行攻击。(利用一个可发起网络请求的服务当作跳板来攻击其他服务)。
我们用一个例子来解释一下这个概念 。想象一下,我们有一个应用程序,用户需要上传图片及图片名称。那么这个时候是使用img
标签显示图片的时候就会先下载图片(向服务器提出请求)。既然攻击者知道我们是在发出这些GET
请求,他就会把http://localhost/admin
这样的网址作为图片名称传递给服务端。如果我们的系统没有考虑到此攻击,那么将向服务器发送一个请求,当有人能够访问我们的内部系统或服务时这是非常危险的。现在。如果我们显示的是图像是下载请求的结果,它可以使攻击者更容易地探索内部服务和做一些事情。
不同类型的ssrf攻击
- 服务器攻击:在上面说到的通过图片传递
url
的例子可知,如果我们传递了类似于localhost
或127.0.0.1
或者服务器本身的IP
,那么我们本身就是在攻击这个服务器。 - 内部后端系统:在这种情况下,攻击者试图与其他后端系统进行通信。为此他使用了包含内部
IP
的地址例如http://192.168.0.68/admin
或者直接使用域名例如https://payment/something
。 - 外部系统:这可能是一个只有我们的服务器或内部服务能够交互的外部系统。这可能是一个银行
API
或任何限制其服务到我们的IP`地址的其他形式的身份验证和授权。
基本ssrf和盲ssrf是什么
在从URL
下载图片的例子中,如果我们直接将响应返回给用户。那么攻击者就能够看到响应并更快地探索我们的内部服务。
如果我们没有显示来自请求的任何响应,那么攻击类型就会变成盲ssrf
,这使得攻击者在没有看到响应的情况下更难探索我们的系统。
ssrf攻击的一个例子
下面是一段简历生成网站代码,当我们为个人网站填写一个字段时。它会检查该字段是否是一个有效的网站。如果它不能访问该网址,会给一个反馈。这个应用程序有一个基本部分,路由。必须对自己进行身份验证才能访问这些路由。我们有一个内部服务被称为付款。只有主要的应用程序可以访问它,而且它不是公开的。(这完全是简单化的,目的是了解可能发生的情况)。
让我们看看代码的核心。它包括:
- 个人网站:查看网址,它会给我们一条信息,告诉我们是否能访问成功,并返回来自个人网站的回应,同时对整个网址提供预览。
- 我的:用于检查当前用户的余额。它向我们的支付服务发送请求。
- 钱包:向我们内部的支付服务发送请求。
const express = require('express');
const app = express();
const axios = require('axios');
app.use(express.json());
const paymentServiceAddress = 'localhost:3011'
app.post('/personal-website', async (req, res) => {const url = req.body?.url;try {const websiteHomePageTest = await axios.get(url);await saveAddressInProfile(url);res.json({ message: 'success', url, data: websiteHomePageTest.data });}catch (error) {if (axios.isAxiosError(error)) {return res.json({ message: 'failed', error: error.message }).status(500);} else {console.error(error);res.json({ message: 'failed' }).status(500);}}
});app.post('/me/balance', someAuthenticationMiddleware, async (req, res) => {const response = await axios.post(`http://${paymentServiceAddress}/1/balance`);const balance = response.data.balance;res.json({ balance });
})app.post('/me/balance/payin', someAuthenticationMiddleware, async (req, res) => {const payIn = await payIn()const response = await axios.post(`http://${paymentServiceAddress}/1/balance/increase`);const balance = response.data.balance;res.json({ balance });
})app.listen(3010, () => console.log(`Example app listening at http://localhost:3010`));
async function saveAddressInProfile(image) { console.log('保存图片'); }
function someAuthenticationMiddleware(req, res, next) { next(); }
async function payIn() { }
如果我们在个人网站上使用一个有效的网址,我们会得到这个结果:
现在,如果我们尝试其他的URL
,可以检查是否具备其他额外的服务。
这个响应给了我们一个内部服务的IP
和port
。现在攻击者应该开始使用这个URL
来寻找其他服务。
如果我们尝试一个地址http://localhost:3011
。我们看到结果是success
,这意味着在这个网址后面有一个服务。让我们看看支付服务的实施情况。我们不能直接向这个服务提出请求,因为它是在一个私人网络中,但是我们可以从主应用程序中提出请求。
const express = require('express');
const app = express();
app.use(express.static('static'));
app.use(express.json());app.get('/', (req, res) => {res.send('pong');
});app.get('/:userId/balance', async (req, res) => {const balance = getUserBalance(req.params.userId);res.json({ balance });
});const users = [{ id: 1, name: 'leo', balance: 100 },{ id: 2, name: 'alex', balance: 200 },{ id: 3, name: 'Jack', balance: 300 }
]
function getUserBalance(userId) {return users.find(user => user.id == userId)?.balance;
}app.listen(3011, () => {console.log(`Example app listening at http://localhost:3011`);
});
在尝试了一点点之后攻击者会发现有一个路径可以获取用户信息。(当URL
不正确时,付款服务返回404,我们在响应中看到结果)。
通过使用balance
路由,我们可以访问所有用户,使用他们的用户名。一般情况下在主程序上使用这个路径/me/balance
,我们可以看到自己的信息,但现在我们也可以未经授权访问其他用户的平衡。
如何防止ssrf攻击
校验
黑名单
可以使用正则表达式来验证URL
,或者列出一个黑名单。列出一些禁止的短语,比如127.0.0.1
或localhost
。我们可以直接使用正则表达式,也可以Zod
、hapi
、validatorjs
这些库。
IP地址
直接限制可访问的ip
。
域名
直接限制可访问的域名。
使用适当的认证和授权
在我们的示例中,看到主应用程序可以访问任何类型的对支付服务的请求。一个更好的方法是传递用户身份验证信息(可能是JWT
或会话或任何东西),然后支付从JWT
有效载荷中获得用户标识,然后我们确定该用户可以访问这些数据。即使是后端服务也应该在彼此之间有有限的访问权限。如果他们依靠用户访问,那么获得未经授权的访问可能会更困难。
不暴露对未知请求的回应
这个例子中的另一个错误是,它在成功和失败时都公开了一个HTTP
请求响应。任何额外信息都可能是攻击者的线索。因此,当个人网站不可用或提供内部服务器时,我们不应该暴露任何相关服务的响应。