最近在维护一个 Node.js 服务时,发现有人试图通过修改 HTTP Host 头来访问内网服务。这种攻击方式看似简单,但危害性不小。今天就来详细聊聊如何通过 Host 头白名单校验来加固你的 Node.js 服务。
HTTP Host 头攻击的原理很简单:攻击者伪造 Host 头,试图绕过域名解析直接访问服务器内网端口。比如把 Host 改为 192.168.1.100:8080,如果服务器不做校验,就可能暴露内网服务。这种攻击在内网渗透中很常见,是 SSRF(服务端请求伪造)攻击的一种形式。
防御的核心就是白名单机制 - 只允许已知的、合法的 Host 访问服务。具体来说:
javascript复制const http = require('http');
// 1. 定义允许访问的 Host 白名单
const ALLOWED_HOSTS = new Set([
'www.example.com', // 业务主域名
'example.com', // 裸域名
'127.0.0.1:3000', // 本地开发环境
'your-server-ip:3000' // 服务器公网IP
]);
// 2. 解析 Host 头(处理端口分离)
function parseHost(rawHost) {
if (!rawHost) return '';
// 分离域名和端口(处理 Host: [::1]:3000 这种 IPv6 格式)
const hostParts = rawHost.split(/:(?=\d+$)/);
return hostParts[0] || rawHost;
}
// 3. 创建 HTTP 服务器并校验 Host 头
const server = http.createServer((req, res) => {
const requestHost = req.headers.host;
// 核心校验逻辑
if (!requestHost) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Forbidden: Missing Host header');
return;
}
// 校验 Host 是否在白名单中
const isAllowed = ALLOWED_HOSTS.has(requestHost) ||
ALLOWED_HOSTS.has(parseHost(requestHost));
if (!isAllowed) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end(`Forbidden: Invalid Host header - ${requestHost}`);
return;
}
// 合法 Host,处理正常业务
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, this is a secure server!');
});
// 启动服务器
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Allowed Hosts: ${Array.from(ALLOWED_HOSTS).join(', ')}`);
});
白名单设计:
Host 解析:
响应设计:
bash复制# 合法请求(允许)
curl -H "Host: www.example.com" http://localhost:3000
# 非法请求(拒绝)
curl -H "Host: 192.168.1.100:8080" http://localhost:3000
# 无 Host 头(拒绝)
curl http://localhost:3000
# IPv6 格式测试
curl -H "Host: [::1]:3000" http://localhost:3000
javascript复制// 通过环境变量配置白名单
const ALLOWED_HOSTS = new Set(
process.env.ALLOWED_HOSTS?.split(',') || [
'localhost:3000',
'127.0.0.1:3000'
]
);
javascript复制function isHostAllowed(host) {
if (ALLOWED_HOSTS.has(host)) return true;
// 检查通配符匹配(如 *.example.com)
for (const pattern of ALLOWED_HOSTS) {
if (pattern.startsWith('*.')) {
const domain = pattern.slice(2);
if (host.endsWith(domain)) return true;
}
}
return false;
}
当使用 Nginx 等反向代理时:
nginx复制# Nginx 配置示例
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
}
}
javascript复制// 记录非法请求
if (!isAllowed) {
console.warn(`[SECURITY] Invalid Host attempt: ${requestHost} from ${req.socket.remoteAddress}`);
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end(`Forbidden: Invalid Host header - ${requestHost}`);
return;
}
问题:本地开发时忘记添加 localhost 到白名单
解决:创建 .env 文件管理开发配置
code复制# .env
ALLOWED_HOSTS=localhost:3000,127.0.0.1:3000
问题:经过代理后 Host 头被修改
解决:检查代理配置,确保正确传递 Host 头
问题:IPv6 地址格式校验失败
解决:更新 parseHost 函数支持 IPv6 格式
javascript复制function parseHost(rawHost) {
// 处理 IPv6 地址(如 [::1]:3000)
const ipv6Match = rawHost.match(/^\[(.+)\](?::(\d+))?$/);
if (ipv6Match) {
return ipv6Match[1]; // 返回 IPv6 地址部分
}
// 其他处理逻辑...
}
在实际项目中,我建议将这套机制封装成中间件,方便在不同服务中复用。比如 Express 的中间件版本:
javascript复制function hostValidator(allowedHosts) {
return (req, res, next) => {
const host = req.headers.host;
if (!allowedHosts.has(host)) {
return res.status(403).send('Invalid Host header');
}
next();
};
}
// 使用
app.use(hostValidator(new Set(['example.com'])));
最后提醒一点:安全是一个持续的过程。除了 Host 校验,还应该考虑其他安全措施如 HTTPS、CSP、CSRF 防护等,构建完整的安全防御体系。