MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js安全实践:防范常见的攻击方式

2022-05-073.4k 阅读

Node.js 安全实践:防范常见的攻击方式

一、SQL 注入攻击防范

  1. SQL 注入攻击原理 SQL 注入攻击是一种常见的 Web 安全漏洞,当应用程序使用用户输入来构造 SQL 查询时,如果未对输入进行适当的验证和转义,攻击者就可以通过精心构造恶意输入,修改 SQL 查询的逻辑,从而获取敏感数据、执行非法操作或破坏数据库。例如,在一个简单的登录验证查询中,正常的 SQL 查询可能是:
SELECT * FROM users WHERE username = 'user' AND password = 'pass';

如果应用程序将用户输入直接拼接到 SQL 查询中,攻击者可以输入 '; DROP TABLE users; --,这样拼接后的查询就变成了:

SELECT * FROM users WHERE username = ''; DROP TABLE users; --' AND password = 'pass';

; 用于分隔 SQL 语句,-- 是注释符,这使得攻击者能够执行额外的 SQL 语句,如删除 users 表。

  1. Node.js 中防范 SQL 注入攻击的方法
    • 使用参数化查询:在 Node.js 中使用数据库驱动(如 mysql2pg 分别用于 MySQL 和 PostgreSQL 数据库)时,应采用参数化查询。以 mysql2 为例:
const mysql = require('mysql2');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'test'
});

const username = 'user';
const password = 'pass';
const query = 'SELECT * FROM users WHERE username =? AND password =?';
connection.query(query, [username, password], (error, results, fields) => {
  if (error) throw error;
  console.log(results);
});

在上述代码中,? 是参数占位符,mysql2 会自动对传入的参数进行转义,防止 SQL 注入。

  • 数据验证和过滤:在将用户输入用于 SQL 查询之前,对输入进行严格的验证和过滤。例如,使用正则表达式验证用户名只能包含字母和数字:
const username = 'user123';
const validUsername = /^[a-zA-Z0-9]+$/.test(username);
if (!validUsername) {
  throw new Error('Invalid username');
}

这样可以确保只有合法的输入才会进入后续的 SQL 查询流程。

二、XSS(跨站脚本攻击)防范

  1. XSS 攻击原理 XSS 攻击允许攻击者在受害者的浏览器中注入恶意脚本。这些脚本可以窃取用户的 cookie、会话令牌或执行其他恶意操作。有两种主要类型的 XSS 攻击:反射型 XSS 和存储型 XSS。

    • 反射型 XSS:攻击者通过诱使用户点击包含恶意脚本的链接,将恶意脚本反射到受害者的浏览器。例如,一个搜索页面接受用户输入并在页面上显示搜索结果,如果应用程序未对输入进行适当的过滤,攻击者可以构造链接 http://example.com/search?q=<script>alert('XSS')</script>,当用户点击该链接时,恶意脚本将在用户浏览器中执行。
    • 存储型 XSS:攻击者将恶意脚本存储在服务器上,当其他用户访问相关页面时,恶意脚本就会被加载并执行。例如,在一个留言板应用中,如果用户留言未经过滤就存储在数据库中,攻击者可以在留言中插入恶意脚本,其他访问留言板的用户就会受到攻击。
  2. Node.js 中防范 XSS 攻击的方法

    • 输出编码:在将用户输入输出到 HTML 页面之前,对其进行编码。在 Node.js 中,可以使用 DOMPurify 库。首先安装 DOMPurify
npm install dompurify

然后在代码中使用:

const DOMPurify = require('dompurify');
const dirty = '<script>alert("XSS")</script>';
const clean = DOMPurify.sanitize(dirty);
console.log(clean);
// 输出:&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

这样,恶意脚本就被转义,无法在浏览器中执行。

  • 输入验证:对用户输入进行严格的验证,限制输入的字符集和长度。例如,对于一个评论输入框,只允许输入字母、数字、空格和一些基本标点符号:
const comment = 'This is a valid comment.';
const validComment = /^[a-zA-Z0-9\s.,!?]+$/.test(comment);
if (!validComment) {
  throw new Error('Invalid comment');
}

同时,设置合理的输入长度限制,防止过长的输入可能包含恶意脚本。

三、CSRF(跨站请求伪造)防范

  1. CSRF 攻击原理 CSRF 攻击迫使已认证的用户在不知情的情况下执行非预期的操作。攻击者利用用户已登录的会话,通过诱使用户访问恶意链接或页面,在用户不知情的情况下向目标网站发送请求。例如,用户已登录到银行网站,此时访问了一个包含恶意 <img> 标签的页面:
<img src="https://bank.com/transfer?to=attacker&amount=1000" style="display:none">

如果银行网站没有适当的 CSRF 防护机制,这个请求可能会被当作合法请求执行,将用户的 1000 元资金转移到攻击者账户。

  1. Node.js 中防范 CSRF 攻击的方法
    • 使用 CSRF 令牌:在用户登录或页面加载时,服务器生成一个唯一的 CSRF 令牌,并将其存储在用户的会话中。同时,将该令牌嵌入到 HTML 表单或页面的 JavaScript 变量中。当用户提交表单或发起 AJAX 请求时,将令牌随请求一起发送到服务器。服务器验证令牌的有效性。以 Express.js 为例,首先安装 csurf 中间件:
npm install csurf

然后在 Express 应用中使用:

const express = require('express');
const csrf = require('csurf');
const app = express();
const csrfProtection = csrf({ cookie: true });

app.use(csrfProtection);

app.get('/form', (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/submit', csrfProtection, (req, res) => {
  // 处理表单提交
  res.send('Form submitted successfully');
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

在上述代码中,csurf 中间件生成并管理 CSRF 令牌。req.csrfToken() 用于获取令牌并传递给视图,csrfProtection 中间件在处理 POST 请求时验证令牌的有效性。

  • 检查 Referer 头:虽然 Referer 头可以被伪造,但在一定程度上也能防范 CSRF 攻击。服务器可以检查请求的 Referer 头,确保请求来自合法的源。在 Express.js 中,可以自定义中间件实现:
const express = require('express');
const app = express();

const checkReferer = (req, res, next) => {
  const allowedOrigin = 'http://your-website.com';
  const referer = req.get('Referer');
  if (referer && referer.startsWith(allowedOrigin)) {
    next();
  } else {
    res.status(403).send('Forbidden');
  }
};

app.post('/submit', checkReferer, (req, res) => {
  // 处理表单提交
  res.send('Form submitted successfully');
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

上述代码中,checkReferer 中间件检查 Referer 头是否来自指定的合法源,如果不是则返回 403 禁止访问。

四、文件上传漏洞防范

  1. 文件上传漏洞原理 文件上传功能如果没有正确实现,可能会导致严重的安全问题。攻击者可以上传恶意脚本文件(如 .php 文件)到服务器,如果服务器对上传的文件处理不当,这些恶意脚本可能会被执行,从而获取服务器的控制权。例如,一个图片上传功能,若未对上传文件的类型和内容进行严格验证,攻击者可以将恶意的 .php 文件伪装成图片文件上传,然后通过访问该文件路径执行恶意脚本。

  2. Node.js 中防范文件上传漏洞的方法

    • 文件类型验证:在接收上传文件时,验证文件的类型。可以通过检查文件扩展名和文件头(MIME 类型)来验证。以 multer 库为例,它是一个常用的 Node.js 文件上传中间件。首先安装 multer
npm install multer

然后进行文件类型验证:

const express = require('express');
const multer = require('multer');

const storage = multer.memoryStorage();
const upload = multer({
  storage: storage,
  fileFilter: (req, file, cb) => {
    const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif'];
    if (allowedMimeTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  }
});

const app = express();

app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).send('No file uploaded');
  }
  res.send('File uploaded successfully');
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

在上述代码中,fileFilter 函数检查文件的 MIME 类型是否在允许的列表中。

  • 文件重命名和存储路径限制:对上传的文件进行重命名,避免使用用户提供的文件名,防止攻击者利用文件名进行攻击。同时,将文件存储在安全的路径下,限制对该路径的访问。例如:
const crypto = require('crypto');
const path = require('path');
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads');
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = crypto.randomBytes(16).toString('hex');
    const fileExtension = path.extname(file.originalname);
    cb(null, uniqueSuffix + fileExtension);
  }
});
const upload = multer({ storage: storage });

上述代码中,filename 函数生成一个唯一的文件名,并使用 path.extname 保留原始文件的扩展名。destination 函数指定文件存储在 uploads 目录下,确保该目录的访问权限设置正确。

五、命令注入攻击防范

  1. 命令注入攻击原理 命令注入攻击发生在应用程序使用用户输入来构造系统命令时,如果未对输入进行适当的验证和转义,攻击者可以注入恶意命令,在服务器上执行任意系统命令。例如,在一个执行系统 ping 命令的 Node.js 应用中,正常的命令可能是:
const { exec } = require('child_process');
const host = 'google.com';
exec(`ping -c 4 ${host}`, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

如果应用程序直接将用户输入拼接到命令中,攻击者可以输入 google.com; rm -rf /,这样拼接后的命令就变成了:

ping -c 4 google.com; rm -rf /

; 用于分隔命令,攻击者可以执行如删除根目录(rm -rf /)这样的危险操作。

  1. Node.js 中防范命令注入攻击的方法
    • 参数化输入:避免直接将用户输入拼接到系统命令中,而是使用参数化的方式。例如,对于 ping 命令,可以使用 child_process.execFile 方法:
const { execFile } = require('child_process');
const host = 'google.com';
execFile('ping', ['-c', '4', host], (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

在上述代码中,execFile 的第二个参数是命令的参数数组,这样可以防止恶意命令注入。

  • 输入验证和过滤:对用户输入进行严格的验证和过滤,只允许合法的字符和值。例如,对于一个接受 IP 地址输入的应用,可以使用正则表达式验证:
const ip = '192.168.1.1';
const validIp = /^((25[0 - 5]|2[0 - 4][0 - 9]|[01]?[0 - 9][0 - 9]?)\.){3}(25[0 - 5]|2[0 - 4][0 - 9]|[01]?[0 - 9][0 - 9]?)$/.test(ip);
if (!validIp) {
  throw new Error('Invalid IP address');
}

这样可以确保只有合法的 IP 地址才能用于后续的系统命令操作。

六、信息泄露防范

  1. 信息泄露类型及原理
    • 敏感数据泄露:包括数据库连接字符串、API 密钥、用户密码等敏感信息。如果这些信息在代码中硬编码或者在日志中明文记录,攻击者可能会获取这些信息并利用它们进行进一步的攻击。例如,数据库连接字符串硬编码在 Node.js 代码中:
const mysql = require('mysql2');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password123',
  database: 'test'
});

如果代码库被泄露,攻击者就可以直接使用这些数据库连接信息访问数据库。

  • 服务器信息泄露:服务器返回的错误信息中可能包含服务器的版本号、操作系统信息等。攻击者可以利用这些信息寻找已知的漏洞进行攻击。例如,一个 Express.js 应用中,未处理的错误返回给客户端详细的堆栈跟踪信息:
const express = require('express');
const app = express();

app.get('/error', (req, res) => {
  throw new Error('Internal server error');
});

app.use((err, req, res, next) => {
  res.status(500).send(err.stack);
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

这样的错误信息可能会暴露服务器使用的技术栈和具体版本,方便攻击者进行针对性攻击。

  1. Node.js 中防范信息泄露的方法
    • 环境变量管理敏感数据:将敏感数据存储在环境变量中,而不是硬编码在代码中。在 Node.js 中,可以使用 dotenv 库来加载环境变量。首先安装 dotenv
npm install dotenv

然后在代码中使用:

require('dotenv').config();
const mysql = require('mysql2');
const connection = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME
});

在项目根目录下创建 .env 文件,内容如下:

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=password123
DB_NAME=test

这样,敏感数据不会直接暴露在代码中,并且在部署时可以通过设置服务器的环境变量来配置。

  • 错误处理和日志管理:在生产环境中,避免将详细的错误堆栈信息返回给客户端。可以自定义错误处理中间件,返回通用的错误信息。例如:
const express = require('express');
const app = express();

app.get('/error', (req, res) => {
  throw new Error('Internal server error');
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Internal server error');
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

同时,对日志进行适当的管理,避免在日志中记录敏感信息。例如,对数据库密码进行掩码处理后再记录日志。

七、拒绝服务(DoS)和分布式拒绝服务(DDoS)攻击防范

  1. DoS 和 DDoS 攻击原理

    • DoS 攻击:攻击者通过耗尽目标服务器的资源(如 CPU、内存、带宽等),使服务器无法正常为合法用户提供服务。例如,攻击者可以发送大量的 HTTP 请求,占用服务器的连接资源,导致其他合法请求无法得到处理。
    • DDoS 攻击:DDoS 攻击是 DoS 攻击的分布式版本,攻击者控制多个被感染的计算机(僵尸网络),向目标服务器发送大量请求,使攻击的规模和破坏力更大。例如,攻击者可以利用僵尸网络向目标 Node.js 服务器发送海量的并发请求,使其不堪重负。
  2. Node.js 中防范 DoS 和 DDoS 攻击的方法

    • 请求速率限制:使用中间件对请求速率进行限制,防止单个客户端发送过多的请求。以 Express.js 为例,可以使用 express-rate-limit 库。首先安装:
npm install express-rate-limit

然后在应用中使用:

const express = require('express');
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100, // 每个 IP 在 15 分钟内最多 100 个请求
  message: 'Too many requests from this IP, please try again later.'
});

const app = express();
app.use(limiter);

app.get('/', (req, res) => {
  res.send('Hello World!');
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

上述代码中,express-rate-limit 中间件限制了每个 IP 在 15 分钟内最多只能发送 100 个请求,超过限制则返回错误信息。

  • 资源监控和弹性伸缩:通过监控服务器的资源使用情况(如 CPU、内存、带宽等),当资源接近耗尽时,自动触发弹性伸缩机制。例如,使用云服务提供商(如 AWS、Azure 等)提供的自动伸缩功能,当检测到服务器负载过高时,自动增加服务器实例,以应对 DoS 或 DDoS 攻击。同时,在 Node.js 应用内部,可以使用 os 模块来获取系统资源信息,并根据资源使用情况进行相应的处理,如限制某些高资源消耗的操作:
const os = require('os');

const cpuUsage = os.loadavg()[0];
if (cpuUsage > 0.8) {
  // 限制某些操作,如关闭一些非关键的功能
}

这样可以在一定程度上应对资源耗尽的情况,提高服务器的稳定性。

八、依赖项安全管理

  1. 依赖项安全问题原理 Node.js 项目通常依赖大量的第三方模块,这些模块可能存在安全漏洞。如果项目使用的依赖项版本过旧,其中的已知漏洞可能会被攻击者利用。例如,某个流行的 Web 框架依赖项存在 XSS 漏洞,而项目没有及时更新该依赖项,攻击者就可以利用这个漏洞对应用进行 XSS 攻击。

  2. Node.js 中管理依赖项安全的方法

    • 定期更新依赖项:使用 npm outdated 命令查看项目中过时的依赖项,并使用 npm updatenpm install <package>@latest 来更新依赖项。例如:
npm outdated
npm update express

这样可以确保项目使用的依赖项是最新的,以获取安全补丁。

  • 使用依赖项扫描工具:如 npm audit,它可以扫描项目的依赖项,查找已知的安全漏洞,并提供修复建议。运行 npm audit 命令后,它会检查 package - lock.json 文件,分析依赖项的版本,并与 npm 官方的安全数据库进行比对。如果发现漏洞,会给出详细的漏洞信息和修复建议。例如:
npm audit

同时,也可以使用一些第三方的依赖项扫描工具,如 Snyk,它不仅可以扫描依赖项漏洞,还能提供持续监控和漏洞修复的集成解决方案。首先安装 Snyk

npm install -g snyk

然后在项目目录下运行:

snyk test

Snyk 会生成详细的报告,指出依赖项中的安全问题以及如何修复。

  • 锁定依赖项版本:在 package - lock.json 文件中,npm 会记录每个依赖项的确切版本号。这样可以确保在不同环境中安装依赖项时,使用的版本一致,避免因依赖项版本差异导致的安全问题。同时,在部署项目时,建议使用 npm install --production 命令,它只会安装 package.jsondependencies 字段的依赖项,而不会安装 devDependencies 中的依赖项,减少潜在的安全风险。

通过以上对各种常见攻击方式的防范措施的介绍,在 Node.js 开发中能够有效提升应用程序的安全性,保护用户数据和服务器的稳定运行。开发者应始终将安全放在首位,持续关注安全动态,及时更新和完善安全策略。