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

JavaScript保障Node非HTTP网络通信安全

2023-02-186.2k 阅读

Node 非 HTTP 网络通信概述

在 Node.js 的生态系统中,虽然 HTTP 协议是最为常见的网络通信方式,用于构建 Web 服务器、APIs 等。然而,还有许多场景需要使用非 HTTP 的网络协议,例如基于 TCP(传输控制协议)、UDP(用户数据报协议)的通信。

TCP 通信

TCP 提供了面向连接、可靠的字节流传输服务。在 Node.js 中,可以使用 net 模块来创建 TCP 服务器和客户端。以下是一个简单的 TCP 服务器示例:

const net = require('net');
const server = net.createServer((socket) => {
  socket.write('Welcome!\n');
  socket.on('data', (data) => {
    console.log('Received: %s', data.toString());
    socket.write('You sent: ' + data);
  });
  socket.on('end', () => {
    console.log('Connection closed');
  });
});
server.listen(8124, () => {
  console.log('Server listening on port 8124!');
});

对应的 TCP 客户端示例:

const net = require('net');
const client = net.connect({ port: 8124 }, () => {
  console.log('Connected to server!');
  client.write('Hello, server!');
});
client.on('data', (data) => {
  console.log('Received from server: %s', data.toString());
  client.end();
});
client.on('end', () => {
  console.log('Connection closed');
});

UDP 通信

UDP 是一种无连接的协议,它不保证数据的可靠传输,但具有速度快、开销小的特点。在 Node.js 中,dgram 模块用于 UDP 通信。以下是一个 UDP 服务器示例:

const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
  console.log('Received %s from %s:%d', msg.toString(), rinfo.address, rinfo.port);
  server.send('You sent: ' + msg, rinfo.port, rinfo.address, (err) => {
    if (err) {
      console.error(err);
    }
  });
});
server.on('listening', () => {
  const address = server.address();
  console.log('Server listening on %j', address);
});
server.bind(41234);

UDP 客户端示例:

const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const message = Buffer.from('Hello, server!');
client.send(message, 0, message.length, 41234, 'localhost', (err, bytes) => {
  client.on('message', (msg, rinfo) => {
    console.log('Received from server: %s', msg.toString());
    client.close();
  });
  if (err) {
    console.error(err);
  }
});

非 HTTP 网络通信面临的安全威胁

数据泄露风险

  1. 数据嗅探:在网络传输过程中,如果数据没有进行加密,攻击者可以通过网络嗅探工具捕获数据包,从而获取敏感信息。例如,在上述 TCP 或 UDP 通信示例中,如果传输的是用户密码、银行账号等敏感数据,一旦被嗅探到,后果不堪设想。
  2. 中间人攻击(MITM):攻击者可以在通信双方之间插入自己,拦截并篡改数据。例如,在 TCP 连接建立过程中,攻击者可以伪装成服务器或客户端,获取双方传输的数据。在 UDP 通信中,虽然没有像 TCP 那样的连接概念,但攻击者同样可以截取并修改数据包。

拒绝服务攻击(DoS 和 DDoS)

  1. TCP SYN 洪水攻击:对于 TCP 服务器,攻击者可以发送大量的 SYN 包,但不完成三次握手,导致服务器的连接队列被填满,无法处理正常的连接请求。在 Node.js 的 TCP 服务器中,如果没有适当的防护措施,很容易受到这种攻击。
  2. UDP 洪水攻击:攻击者可以向 UDP 服务器发送大量的伪造 UDP 数据包,消耗服务器的带宽和资源,导致服务器无法正常工作。由于 UDP 协议的无连接特性,这种攻击更加难以防范。

恶意代码注入

  1. 命令注入:如果在接收和处理数据的过程中,没有对输入进行严格的验证和过滤,攻击者可能会注入恶意命令。例如,在上述 TCP 服务器示例中,如果直接将接收到的数据作为系统命令执行,攻击者就可以通过发送恶意命令来控制服务器。
  2. 代码注入:类似地,攻击者也可能尝试注入 JavaScript 代码。如果服务器在处理数据时,存在动态执行代码的情况,且没有对输入进行安全处理,就可能导致代码注入攻击。

使用加密技术保障通信安全

TLS/SSL 加密

在 Node.js 中,可以使用 tls 模块为 TCP 通信添加 TLS/SSL 加密。TLS(传输层安全协议)及其前身 SSL(安全套接层协议)通过对数据进行加密和身份验证,有效防止数据泄露和中间人攻击。

以下是一个使用 tls 模块的 TCP 服务器示例:

const tls = require('tls');
const fs = require('fs');
const options = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  requestCert: true,
  rejectUnauthorized: true
};
const server = tls.createServer(options, (socket) => {
  socket.write('Welcome!\n');
  socket.on('data', (data) => {
    console.log('Received: %s', data.toString());
    socket.write('You sent: ' + data);
  });
  socket.on('end', () => {
    console.log('Connection closed');
  });
});
server.listen(8125, () => {
  console.log('TLS Server listening on port 8125!');
});

对应的客户端示例:

const tls = require('tls');
const fs = require('fs');
const options = {
  key: fs.readFileSync('client-key.pem'),
  cert: fs.readFileSync('client-cert.pem'),
  ca: [fs.readFileSync('ca-cert.pem')]
};
const client = tls.connect(8125, 'localhost', options, () => {
  console.log('Connected to TLS server!');
  client.write('Hello, server!');
});
client.on('data', (data) => {
  console.log('Received from server: %s', data.toString());
  client.end();
});
client.on('end', () => {
  console.log('Connection closed');
});

在上述示例中,服务器和客户端都需要配置相应的证书和密钥。requestCertrejectUnauthorized 选项用于要求客户端提供证书并验证其合法性。通过这种方式,通信数据在传输过程中被加密,攻击者无法直接获取明文数据。

对称加密

除了 TLS/SSL 这种基于证书的加密方式,还可以使用对称加密算法对数据进行加密。在 Node.js 中,crypto 模块提供了丰富的加密功能。

以下是一个使用 AES - 256 - CBC(高级加密标准,256 位密钥,密码块链接模式)对称加密算法的示例:

const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
function encrypt(text) {
  const cipher = crypto.createCipheriv(algorithm, key, iv);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
}
function decrypt(text) {
  const decipher = crypto.createDecipheriv(algorithm, key, iv);
  let decrypted = decipher.update(text, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}
const plaintext = 'Hello, this is a secret message';
const encryptedText = encrypt(plaintext);
const decryptedText = decrypt(encryptedText);
console.log('Plaintext:', plaintext);
console.log('Encrypted:', encryptedText);
console.log('Decrypted:', decryptedText);

在实际的网络通信中,可以在发送端对数据进行加密,在接收端进行解密。这样即使数据被截取,攻击者也无法直接获取明文内容。

防范拒绝服务攻击

TCP SYN 洪水攻击防范

  1. SYN 饼干(SYN Cookies):这是一种防范 SYN 洪水攻击的有效方法。在 Node.js 中,可以通过第三方模块或者自行实现相关逻辑。当服务器接收到 SYN 包时,不立即分配资源建立连接,而是根据特定算法生成一个特殊的序列号(饼干),并将其作为 SYN + ACK 包的初始序列号发送给客户端。当客户端返回 ACK 包时,服务器验证该序列号的合法性。如果合法,则建立连接。
  2. 设置连接队列大小:合理设置 TCP 服务器的连接队列大小。在 Node.js 的 net 模块中,可以通过 server.listen 方法的第二个参数设置 backlog(连接队列大小)。例如:
const net = require('net');
const server = net.createServer((socket) => {
  // 处理连接
});
server.listen(8124, 511, () => {
  console.log('Server listening on port 8124 with backlog 511!');
});

适当调整 backlog 的值,可以在一定程度上防止连接队列被填满。

UDP 洪水攻击防范

  1. 速率限制:对于 UDP 服务器,可以实施速率限制。通过记录每个源 IP 地址的数据包发送速率,当速率超过一定阈值时,丢弃来自该 IP 的数据包。在 Node.js 中,可以使用 Map 数据结构来记录每个 IP 的发送速率。以下是一个简单的速率限制示例:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
const rateLimit = new Map();
const MAX_RATE = 100; // 每秒最大数据包数
const RATE_WINDOW = 1000; // 速率统计窗口,单位毫秒
let lastUpdate = Date.now();
server.on('message', (msg, rinfo) => {
  const now = Date.now();
  if (now - lastUpdate > RATE_WINDOW) {
    rateLimit.clear();
    lastUpdate = now;
  }
  if (!rateLimit.has(rinfo.address)) {
    rateLimit.set(rinfo.address, 1);
  } else {
    const count = rateLimit.get(rinfo.address);
    if (count >= MAX_RATE) {
      // 丢弃数据包
      return;
    }
    rateLimit.set(rinfo.address, count + 1);
  }
  // 处理正常数据包
  console.log('Received %s from %s:%d', msg.toString(), rinfo.address, rinfo.port);
  server.send('You sent: ' + msg, rinfo.port, rinfo.address, (err) => {
    if (err) {
      console.error(err);
    }
  });
});
server.on('listening', () => {
  const address = server.address();
  console.log('Server listening on %j', address);
});
server.bind(41234);
  1. IP 白名单和黑名单:维护一个 IP 白名单,只接受来自白名单内 IP 地址的数据包。同时,可以将已知的攻击源 IP 地址加入黑名单,拒绝来自这些 IP 的数据包。在 Node.js 中,可以使用数组来实现简单的白名单和黑名单机制。例如:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
const whiteList = ['192.168.1.100', '10.0.0.5'];
const blackList = ['172.16.0.254'];
server.on('message', (msg, rinfo) => {
  if (blackList.includes(rinfo.address)) {
    // 丢弃数据包
    return;
  }
  if (!whiteList.includes(rinfo.address)) {
    // 丢弃数据包
    return;
  }
  // 处理正常数据包
  console.log('Received %s from %s:%d', msg.toString(), rinfo.address, rinfo.port);
  server.send('You sent: ' + msg, rinfo.port, rinfo.address, (err) => {
    if (err) {
      console.error(err);
    }
  });
});
server.on('listening', () => {
  const address = server.address();
  console.log('Server listening on %j', address);
});
server.bind(41234);

防止恶意代码注入

输入验证和过滤

  1. 正则表达式验证:对于接收到的数据,使用正则表达式进行验证。例如,如果期望接收的是数字,可以使用以下代码进行验证:
function validateNumber(input) {
  return /^\d+$/.test(input);
}
const input = '123';
if (validateNumber(input)) {
  console.log('Valid number');
} else {
  console.log('Invalid input');
}

在实际应用中,对于 TCP 或 UDP 通信接收到的数据,可以在处理前先进行这样的验证。 2. 白名单过滤:只允许特定的字符或字符集。例如,只允许字母和数字,可以使用以下代码:

function filterAlphaNumeric(input) {
  return input.replace(/[^a-zA-Z0-9]/g, '');
}
const input = 'abc123!@#';
const filtered = filterAlphaNumeric(input);
console.log('Filtered:', filtered);

避免动态代码执行

  1. 预编译模板:如果需要根据用户输入生成代码或执行特定逻辑,尽量使用预编译模板。例如,在处理 HTML 模板时,可以使用 handlebars 等模板引擎。这些模板引擎会对输入进行安全处理,避免代码注入风险。
  2. 严格限制 eval 等函数的使用eval 函数可以动态执行 JavaScript 代码,这是非常危险的。除非绝对必要,否则应避免使用 eval。如果一定要使用,要确保传入 eval 的数据是完全可信的。

安全的网络通信架构设计

分层架构

  1. 网络层:在网络层,可以采用防火墙等设备来限制外部网络对内部网络的访问。在 Node.js 应用中,可以通过配置服务器的网络策略,只允许特定端口和 IP 地址的连接。
  2. 传输层:如前文所述,在传输层使用加密技术,如 TLS/SSL 或对称加密,确保数据传输的安全性。
  3. 应用层:在应用层,实施输入验证、访问控制等安全措施。例如,对于不同的用户角色,限制其对特定功能的访问。

微隔离

将应用程序分解为多个微服务,并对每个微服务实施严格的访问控制。在 Node.js 微服务架构中,可以使用服务网格(如 Istio)来实现微隔离。每个微服务之间的通信可以通过加密通道进行,并且只有经过授权的微服务之间才能进行通信。

安全审计和监控

  1. 日志记录:在 Node.js 应用中,使用日志记录所有的网络通信活动。通过分析日志,可以及时发现异常行为,如大量的连接请求、异常的数据传输等。例如,使用 winston 等日志库:
const winston = require('winston');
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transport.Console()
  ]
});
// 在网络通信相关代码中记录日志
const net = require('net');
const server = net.createServer((socket) => {
  logger.info('New connection established');
  socket.on('data', (data) => {
    logger.info('Received data: %s', data.toString());
  });
  socket.on('end', () => {
    logger.info('Connection closed');
  });
});
server.listen(8124, () => {
  logger.info('Server listening on port 8124');
});
  1. 实时监控:使用监控工具实时监测网络流量、资源使用情况等。例如,使用 prometheusgrafana 搭建监控系统,及时发现并报警可能的安全威胁。

通过上述一系列措施,可以在 JavaScript 的 Node.js 环境中,有效地保障非 HTTP 网络通信的安全,降低各种安全风险,确保应用程序的稳定运行和数据的安全。