express中间件当做前端服务器的安全漏洞处理

这篇具有很好参考价值的文章主要介绍了express中间件当做前端服务器的安全漏洞处理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

使用express当做node服务器时,发现安全漏洞,记录处理步骤:
PS:以下安全内容处理,需要使用到redis进行会话存储、请求计数、请求唯一限制等。为尽量确保开发环境与部署环境一致,请开发环境安装并启动Redis服务。
** 此文档只是说明记录关键步骤。具体实现代码可参照附件。**

1、cookie没有加签、缺少sameSite参数

  • 使用clientKey给cookie加签,所有通过res.cookie()方法设置cookie的地方都需要设置,此处只是演示:
// app.js
...
// 设置cookie时,如果需要使用签名加密,必须在此处设置签名字符串
app.use(cookieParser(clientKey));
...
...
// 修改前
res.cookie('C2AT', data.access_token, { maxAge: parseInt(data.expires_in) * 1000});

// 修改后
res.cookie('C2AT', data.access_token, { maxAge: parseInt(data.expires_in) * 1000,sameSite:true,signed:true });
...
  • 加签后的cookie获取方式需要修改:
...
// 加签前
let c2at = req.cookie.C2AT;

// 加签后
let c2at = req.signedCookies.C2AT;
...

2、图形验证码登录,抓包登录接口后使用相同验证码登录(Replay)成功

原因: express使用了cookie-session中间件,该中间件是将session数据存放在了cookie之中,导致使用相同的cookie时,sessoin中拿到的验证码是同一个。
解决方案:
使用express-session中间件,将session存放在服务器,cookie中只有sessionId。中间件文档:https://express.nodejs.cn/en/resources/middleware/session.html
demo项目环境:node V16.19.0、express ^4.15.5,版本不一致,插件版本可能也不一致(插件部分API可能不一致),根据各自项目版本适配。
此处使用redis数据库作为session存储,根据项目不同,还可以使用其他,具体参照文档修改关键位置。

  • 依赖包准备:
npm install express-session@1.15.6 redis@3.1.2 connect-redis@6.1.3 dotenv@16.0.3
  • 环境变量,在express项目根目录下新建.env文件,在该文件中添加环境变量(使用云平台部署方式,该文件中的环境变量会被云平台注入的同名环境变量覆盖)
// .env
# redis配置
redis_host=127.0.0.1
redis_port=6379
redis_db=10
  • 关键代码
// app.js
// 引入环境变量,尽可能早
require('dotenv').config();
var express = require('express');
var app = express();
// 将session信息存放在服务端,必须要设置数据库保存session
var session = require('express-session');
// 链接redis
var RedisStore = require("connect-redis")(session);
// redis 数据库
const redis = require("redis");
...
...
// 初始化redis
const redisClient = (function(){
    let client = redis.createClient({
        url:`redis://${process.env.redis_host || '127.0.0.1'}:${process.env.redis_port || '6379'}/${process.env.redis_db || '0'}`,
        // username:'',
        // password:'',
    });
    client.on("error", function(error) {
        console.error('redis链接失败',error)
    });
    client.on("connect", function() {
        console.log('redis链接成功')
    });
    return client;
})();
// 初始化session使用的store
const redisStore = (function(){
    let store = undefined;
    // 部署环境必须指定外部存储会话
    if(process.env.NODE_ENV === 'production'){
        store = new RedisStore({
            client: redisClient,//redis客户端,必须要指定db
            prefix: "webSession:",//在redis中的key名
        });
        // 初始化时,从存储中删除所有会话
        store.clear();
    }
    return store;
})();
// 初始化session中间件,会话信息存储在服务器,只在cookie中设置sessionid
app.use(session({
    secret: clientKey,//对session数据进行加密的字符串.这个属性值为必须指定的属性(使用cookieParser时,两个中间件的签名需要一致)
    resave: true, // session(cookie中存在sessionId的session)没有被修改,也保存session
    saveUninitialized: false, // 强制将“新的且未修改”的会话保存到存储中。 
    rolling:false,//强制在每个响应上设置会话标识符 cookie。 到期重置为原来的maxAge,重置到期倒计时。默认值为false。
    cookie: { 
        maxAge: 1000 * 60 * 60 * 24 * 7, 
        signed: true,
        sameSite:true,//是否为同一站点的cookie
    },
    store:redisStore
}));
...
...
// 登出操作,清空cookie,前端再执行重定向
app.post('/check_out', function (req, res) {
	...
    req.session.destroy()
	...
});
...
// 获取用户信息
app.get('/user_info', function (req, res) {
    ...
    UserUtils.commonCheckToken(res, req).then((tokenInfo) => {
        // 修改session内容,触发session保存到redis、并向res cookie中设置sessionid
        req.session.userId=tokenInfo.userId;
		...
		...
...
  • 初始化express-session中间件后,在接口中正常使用req.session 即可。

3、抓包后,更改接口参数请求成功(参数篡改)、重复请求接口(Replay)请求成功(接口重放)

基于session进行参数加签,请确保已经完成问题2的处理,同时需要前端配合使用MD5对关键接口加签(通过浏览器重定向访问的接口无法处理,需要在.env 中添加request_sign_ignore 忽略接口检查)

  • 环境变量,在.env中添加:
// .env
# 请求签名启用(1启用、默认关闭)ps:因为前端也需要这个变量所以添加前缀custom_
custom_request_sign_enable=1
# 请求签名超时毫秒(同一个请求客户端时间戳和服务器时间戳的过期阈值)
request_sign_time_out=30000
# 需要签名的请求正则字符串
request_sign_reg=\/proxy|\/product-im|\/other-anonymous|\/uploadsFiles|\/downloadFiles|\/qrcode|\/qrcode\/scan|\/custom-code|\/custom-login|\/oauth2-login|\/end-login|\/check_out|\/user_info|\/custom\/env|\/company
# 忽略签名校验的请求正则字符串
request_sign_ignore=\/oauth2-login|\/downloadFiles
  • app.js 关键代码
...
// 初始化私钥和公钥(!!!注意:私钥不能暴露到外部,必须保留在服务器)
const rsaPublicKey = encryUtils.rsa_publicKey();
global.rsaPrivateKey = encryUtils.rsa_privateKey();
// 初始化自定义环境变量
const custom_env = (function(){
    const envs= process.env || {};
    const customEnvs = {};
    Object.keys(envs).forEach(key => {
        // 注意:只获取自定义的环境变量,其他环境变量可能包含服务器信息,通过接口返回可能不安全,请谨慎处理
        if(typeof key === 'string' && key.startsWith('custom_')){
            customEnvs[key] = envs[key]
        }
    });
    return customEnvs;
})();
...
...
// 调试开发时进行跨域设置
app.use(cors({
	...
    headers: 'Authorization,x-requested-with,content-type,content-length,paramSign,sign,requestTime',// 开发环境跨域,需要添加paramSign,sign,requestTime允许跨域
}));
//应用的每个请求都会执行该中间件
app.use(function (req, res, next) {
	...
    // 修改session内容,触发session保存到redis、并向res cookie中设置sessionid
    req.session.lastTime=new Date().getTime();
	next();
});
// request 有效性验证
app.use(new RegExp(process.env.request_sign_reg),function(req, res, next){
    if(process.env.custom_request_sign_enable === '1'){
        if(process.env.request_sign_ignore && new RegExp(process.env.request_sign_ignore).test(req.originalUrl)){
            // 存在忽略请求配置,且忽略正则匹配成功 则不校验
            next();
        }else{
            validRequest({
                req,
                res,
                rsaPrivateKey,
                redisClient
            }).then(res => {
                logger.info('validRequest success ----------> ' + req.originalUrl);
                next();
            }).catch((err) => {
                console.error('validRequest fail ----------> ',req.originalUrl,err)
                const e = typeof err === 'string' ? {status:'500',errorCode:'error',errorMessage:err} : err;
                res.status(e.status).json(e);
            });
        }
    }else{
        next();
    }
})
...
...
//前端html模板引用configuration.js将环境变量等信息注入到前端全局变量
app.get('/configuration.js', function (req, res) {
    // 获取自定义环境变量
    function loadCustomEnvVar(){
        let customEnvs = '';
        Object.keys(custom_env).forEach(key => {
                customEnvs += `${key}="${custom_env[key]}",`;
        });
        customEnvs = customEnvs.substring(0,customEnvs.lastIndexOf(','));
        return customEnvs;
    }
    res.setHeader('Content-type', 'application/javascript; charset=UTF-8');
    res.send(`const ${loadCustomEnvVar()},pubKey="${rsaPublicKey}";`);
});
...
  • express服务器EncryptionUtils.js 加密工具,见附件:express:EncryptionUtils.js

  • ValidateUtils.js 请求有效性校验工具,见附件:express:ValidateUtils.js

  • 前端参数签名关键代码,request.ts

// request.ts
...
...
// 请求拦截器,为请求加签
request.interceptors.request.use((url, options) => {
  const { params, data, } = options;
  const query = getQueryObject(url);
  const { paramSign, sign, requestTime } = requestSign({ ...query, ...params }, data);
  return {
    url, options: {
      ...options,
      headers: {
        ...options.headers,
        paramSign,
        sign,
        requestTime,
      }
    }
  }
});
...
...

/**
 * 请求签名
 * @param params query参数对象
 * @param body body参数对象
 * @returns 
 */
export const requestSign = (params: object, body: object) => {
// 没有开启请求签名校验(要使用自定义环境变量,首先应当引入全局configuration.js)
  if (getCustomEnv('custom_request_sign_enable') !== '1') {
    return {};
  }
  // 时间戳签名
  const signtimestamp = new Date().getTime().toString();
  // 参数签名
  const sign = default_md5_key + signtimestamp;
  // 参数转能签名的字符串
  const signPramsStr = fomartSignParams2String(params, body);
  return {
    paramSign: md5_encode(signPramsStr, sign),
    sign: rsa_pub_encode(sign),
    requestTime: signtimestamp,
  }
}
  • 前端附件上传接口签名:
// 三种解决方案
// 1、为每个antd的Upload组件添加签名内容
// 2、自定义组件包装antd的Upload组件,并为其添加签名内容,其他地方用到的Upload组件统一使用自定义的
// 3、express忽略接口中添加附件上传相关接口正则
...
import { requestSign } from '@/utils/request';
...
<Upload headers={requestSign(...)}>...</Upload>
  • 前端EncryptionUtils.js加密工具,见附件web:EncryptionUtils.js

  • 前端获取express中的环境变量函数:

// config.ts
...
const { 
	...
	NODE_ENV 
} = process.env;
const isDev = NODE_ENV === 'development';
...
export default {
...
context: {
    customConfigPath: (isDev ? constant.express : '') + '/configuration.js',
  },
...
}
// document.ejs
...
<head>
	...
	<script src="<%=context.customConfigPath %>"></script>
</head>
...
...
// utils.ts
...
/**
 * 获取express提供的配置
 * @param name 配置的key
 * @returns 配置的值
 */
export const getCustomEnv = (name: string) => {
  if (typeof name !== 'string') {
    return '';
  }
  try {
    return eval(name) || '';
  } catch (error) {
    return '';
  }
}
...
...

修改完成后,具体实现效果应当为

  • 前端签名外的地方直接请求express的接口会被拦截(如:浏览器直接访问接口、postman请求接口)
  • 抓包后replay会被拦截

4、登录、修改密码等敏感数据未加密

  • 生成RSA秘钥对:参考使用openssl生成RSA秘钥:https://blog.csdn.net/qq_37819292/article/details/136320969
  • 敏感数据手动加密是有必要的,但数据传输的安全性不要依赖手动加密,请启用HTTPS
  • express将RSA公钥传输给前端(在第3条中已经添加,此处只展示关键代码,不用重复添加)
...
// 初始化私钥和公钥(!!!注意:私钥不能暴露到外部,必须保留在服务器)
const rsaPublicKey = encryUtils.rsa_publicKey();
global.rsaPrivateKey = encryUtils.rsa_privateKey();
// 初始化自定义环境变量
const custom_env = (function(){
    const envs= process.env || {};
    const customEnvs = {};
    Object.keys(envs).forEach(key => {
        // 注意:只获取自定义的环境变量,其他环境变量可能包含服务器信息,通过接口返回可能不安全,请谨慎处理
        if(typeof key === 'string' && key.startsWith('custom_')){
            customEnvs[key] = envs[key]
        }
    });
    return customEnvs;
})();
...
...
app.get('/configuration.js', function (req, res) {
    // 获取自定义环境变量
    function loadCustomEnvVar(){
        let customEnvs = '';
        Object.keys(custom_env).forEach(key => {
                customEnvs += `${key}="${custom_env[key]}",`;
        });
        customEnvs = customEnvs.substring(0,customEnvs.lastIndexOf(','));
        return customEnvs;
    }
    res.setHeader('Content-type', 'application/javascript; charset=UTF-8');
    res.send(`const ${loadCustomEnvVar()},pubKey="${rsaPublicKey}";`);
});
...
  • 前端使用RSA公钥加密账号密码等敏感信息(只是登录、修改密码的信息,RSA加密即可;其他地方加密内容过多时,使用RSA+AES的方式)
// login.ts 登录信息加密
...
import { rsaEncodeBodyInfo } from '@/utils/EncryptionUtils';
...
* login({ payload }, { call, put }) {
        // 登录信息加密
        let paramObj = rsaEncodeBodyInfo(payload);
		let response = yield call(fakeAccountLogin, paramObj);
		...
    },
...
...
// user.ts 修改密码信息加密
...
import { rsaEncodeBodyInfo } from '@/utils/EncryptionUtils';
...
*modifyModifyPwd({ payload, callback }, { call }) {
  const params = rsaEncodeBodyInfo(payload);
  const response = yield call(updateModifyPwd, params);
  if (callback) callback(response);
},
...
...
  • express使用RSA私钥解密后登录、修改密码
// app.js
...
var encryUtils = require('./utils/EncryptionUtils');
...
// 统一认证登录
app.post('/custom-login', function (req, res) {
    const bodyMap = encryUtils.RsaDecodeBodyInfo(req.body,rsaPrivateKey);
    const {sn,type,userName,password,code} = bodyMap;
	...
});
...
...
// proxy.js
...
const { RsaDecodeBodyInfo } = require('../utils/EncryptionUtils');
...
// 修改密码要对字段信息进行解密
router.use("/edp/v1/users/loginModifyPwd", function (req, res, next) {
    req.body = RsaDecodeBodyInfo(req.body,global.rsaPrivateKey);
    next();
});
router.use("/", function (req, res, next) {
...
}
...

5、缺少安全相关Header设置

使用helmet可以快速设置安全相关的Header,显著地提升你应用的安全性

  • 安装插件
npm install helmet
  • 使用
// app.js
...
// 设置与安全相关的 HTTP 响应标头
const helmet = require('helmet');
...
var app = express();
// 删除x-powered-by 响应头
app.set('x-powered-by',false) 
// 设置与安全相关的 HTTP 响应标头
app.use(helmet({ 
    // 跨域资源策略 "same-origin" | "same-site" | "cross-origin" 
    crossOriginResourcePolicy: { policy: "same-site" },
}));
...

6、暴力请求,可无限次对服务器发起请求

通过循环等方式对接口暴力请求,常见登录密码暴力破解等脚本攻击。使用rate-limiter-flexible可以限制用户/IP对接口的访问速率,超过访问速率进行访问限制。文章来源地址https://www.toymoban.com/news/detail-843671.html

  • 安装插件
npm install rate-limiter-flexible
  • 环境变量
//.env
# 请求速率限制启用(1启用、默认关闭)
request_limit_enable=1
# 请求速率限制周期内可消耗计数点
request_limit_points=60
# 请求速率限制周期内最小访问次数限制(/proxy 和 /product-im 的接口外受此限制)
request_limit_min_count=5
# 请求速率限制重置周期s
request_limit_duration=5
# 请求速率超过计数点锁定时长s
request_limit_blockDuration=60
  • 使用
// app.js
...
// 初始化redis客户端
const redisClient = redis.createClient({
    url:`redis://${redis_host}:${redis_port}/${redis_db}`,
    // username:'',
    // password:'',
});
redisClient.on("error", function(error) {
    console.error('redis链接失败',error)
});
redisClient.on("connect", function() {
    console.log('redis链接成功')
});
...
// 处理请求静态资源中的gzip文件
app.use(function (req, res, next) {
...
});
//请求速率限制
const rateLimiter = (function(){
    let limiter = undefined;
    if(request_limit_enable === '1'){
        limiter = new RateLimiterRedis({
            storeClient: redisClient,
            keyPrefix: 'rateLimiter',
            points: Number(request_limit_points), // 限制周期内的可消耗计数点
            duration: Number(request_limit_duration), // 重置计数器周期
            blockDuration:Number(request_limit_blockDuration),// 超过计数点的锁定时间
            // inMemoryBlockOnConsumed:Number(request_limit_points),// 超过设置的点时,阻止向存储添加计数器
            // inMemoryBlockDuration:2,// 阻止向存储添加计数器的时间
            insuranceLimiter:new RateLimiterMemory({
                points: Number(request_limit_points), 
                duration: Number(request_limit_duration),
            }),//保险,只有当外部存储无法使用时生效
        });
    }
    return limiter;
})() 
...
// IP请求速率限制
app.use(function(req, res, next){
    if(!rateLimiter){
        next();
    }else{
        // 总点数
        const points = Number(request_limit_points);
        const minCout = Number(request_limit_min_count);
        // 请求消耗的计数点(/proxy 和 /product-im 的接口每次消耗一个计数点,其他类型的接口周期内只能请求5次)
        let pointsToConsume = req.path.includes('/proxy') || req.path.includes('/product-im') ? 1 : Math.floor(points / minCout);
        rateLimiter.consume(req.ip,pointsToConsume)
          .then(() => {
            next();
          })
          .catch(() => {
            logger.error('rateLimiter fail ----------> ' + req.originalUrl)
            res.status(429).json({errorMessage:'请求过快,请稍后再试'});
          });
    }
});
// request 有效性验证
app.use(new RegExp(request_sign_reg),function(req, res, next){
...

到了这里,关于express中间件当做前端服务器的安全漏洞处理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 初识express/路由/中间件

                                     ​​​​​​​        

    2024年02月11日
    浏览(64)
  • node中间件-express框架

    方式一 : express提供的脚手架,直接创建一个应用的骨架 安装脚手架npm install -g express-generator 创建项目 express express-demo 安装依赖npm install 启动项目 node bin/www 方式二 : 从零搭建自己的express应用结构; 初始化项目 npm init 安装express npm i express 导入–创建–监听 使用参考文档 中

    2024年02月16日
    浏览(57)
  • 93 # 实现 express 错误处理中间件

    上一节实现了 express 的中间件,这一节来实现错误处理中间件 执行某一步出错了,统一规定调用 next 传递的参数就是错误信息 先看 express 实现的demo 然后去访问: http://localhost:3000/ 错误处理中间价,里面必须要有 4 个 参数(取函数的长度),放到栈的最底下 下面实现处理逻

    2024年02月07日
    浏览(38)
  • 【Express】文件上传管理 multer 中间件

    Multer是Node.js中用于处理文件上传的中间件。它可以帮助你处理文件上传的相关逻辑,如接收和保存上传的文件、限制文件大小、设置文件类型限制等。只能用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。 下面是使用Multer中间件的基本步骤: 安装multer:在命

    2024年02月07日
    浏览(43)
  • 编写中间件以用于 Express 应用程序

    中间件 函数能够访问请求对象 ( req )、响应对象 ( res ) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为  next  的变量来表示。 中间件函数可以执行以下任务: 执行任何代码。 对请求和响应对象进行更改。 结束请求/响应循环。 调用堆栈

    2024年02月10日
    浏览(41)
  • Go重写Redis中间件 - GO实现TCP服务器

    首先新建一个项目go-redis,将config和lib包放到项目中,config.go用来解析配置,比如端口、功能、DB数;lib包有两个文件夹,分别是logger和sync,其中logger.go是一个日志框架,sync包中的bool.go包装了atomic操作,因为atomic原生没有bool类型,所以将uint32类型改造成bool型的atomic,wait.g

    2024年02月15日
    浏览(83)
  • express学习笔记5 - 自定义路由异常处理中间件

    修改router/index.js,添加异常处理中间件 完整代码 创建 utils/constant:(为了方便后期统一维护,单独拉出来定义) 然后刷新http://localhost:8000/user  这就完成了

    2024年02月14日
    浏览(48)
  • 中间件安全:Apache Tomcat 弱口令.(反弹 shell 拿到服务器的最高控制权.)

    Tomcat  是 Apache 软件基金会(Apache Software Foundation)的 Jakarta 项目中的一个核心项目,由 Apache、Sun 和其他一些公司及个人共同开发而成。 通过弱口令登录后台,部署 war 包 geshell . 中间件安全:Apache Tomcat 弱口令. Apache Tomcat 弱口令: 靶场准备:Web安全:Vulfocus 靶场搭建.(漏

    2024年02月05日
    浏览(65)
  • node 第十四天 基于express的第三方中间件multer node后端处理用户上传文件

    Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。它是写在 busboy 之上的所以非常高效。 前面我们已经知道了怎样利用express提供的静态资源处理中间件 express.static() 处理用户请求静态资源文件(图片, js, css等) 接下来学习如何处理用

    2024年02月06日
    浏览(43)
  • 前端中间件Midway的使用

    Midway 是阿里巴巴 - 淘宝前端架构团队,基于渐进式理念研发的 Node.js 框架,通过自研的依赖注入容器,搭配各种上层模块,组合出适用于不同场景的解决方案。 Midway 基于 TypeScript 开发,结合了面向对象(OOP + Class + IoC)与函数式(FP + Function + Hooks)两种编程范式,并在此之

    2024年02月06日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包