目录

使用文本过滤的方式防御XSS

​ 跨站脚本攻击(Cross Site Scripting),缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的

1.XSS的原理

攻击者对含有漏洞的服务器发起XSS攻击(注入JS代码),诱使受害者打开受到攻击的服务器URL,受害者在Web浏览器中打开URL,恶意脚本执行

2.防御方式

这里介绍的是文本过滤的一种方式来防御XSS

首先定义一个配置文件,规定哪些请求需要进行文本过滤,如:

1
2
3
4
5
6
7
{
    "xss": {
        "post:/api/test/": {
            "content": "close"
        }
    }
}

引入此配置文件,请求时,查看当前请求是否在进行文本过滤的列表里,如果是,将当前参数里面的敏感字符、带有html字符的进行转义,并重新生成参数。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
'use strict';

/**
 * XSS过滤方法
 */

const xss = require('xss'); // xss模块
const url = require('url'); // url模块
const safeConfig = require('./config'); // 上面的需要过滤的配置文件

//默认配置,删除不在白名单中的标签和属性
const DEFAULT_CONFIG = {
  stripIgnoreTag: true,
  stripIgnoreTagBody: true
};

/**
 * 用js-xss 对html过滤
 *
 * @param  {String} str  过滤前字符串
 * @return {String} str  过滤后字符串
 */
function filterHtml(str) {
  const xssObj = new xss.FilterXSS(DEFAULT_CONFIG);
  return xssObj.process(str);
}

exports.filterHtml = filterHtml;

/**
 * 参数过滤,对敏感字符进行转义
 *
 * @param {String} str        过滤前字符串
 * @return {object}
 *   - {Boolean} isMatched    是否匹配到敏感字符
 *   - {String}  value        过滤后的字符串
 *   - {String}  matchedChar  敏感字符
 */
function escapeParams(str) {
  let isMatched = false;
  let matchedChar = '';
  let replacedStr = str.trim(); // 去除前后空格
  replacedStr = str.replace(/[<>'"\\]/g, function (e) {
    isMatched = true;
    matchedChar = e;
    return '&#' + e.charCodeAt() + ';';
  });

  return {
    isMatched: isMatched,
    value: replacedStr,
    matchedChar: matchedChar
  };
}

exports.escapeParams = escapeParams;

/**
 * 判断是否是html结尾的参数名
 *
 * @param {String} param
 * @return         {Boolean}
 */
function isHtmlParam(param) {
  const REGEXP_HTML = /(HTML)$/i;
  return REGEXP_HTML.test(param);
}

/**
 * 重置路由参数
 * @param  {Object} req     请求对象
 * @param  {Object} ruleMap 规则索引
 */
function rebuildReq(req, rule) {
  // 过滤query
  Object.keys(req.query).forEach(function (key) {
    req.query[key] = rebuildParam(req.query[key], rule[key] || isHtmlParam(key));
  });
  // 过滤body
  if (Array.isArray(req.body)) {
    req.body.forEach(function (item, index) {
      Object.keys(item).forEach(function (key) {
        req.body[index][key] = rebuildParam(req.body[index][key], rule[key] || isHtmlParam(key));
      });
    });
  } else {
    Object.keys(req.body).forEach(function (key) {
      req.body[key] = rebuildParam(req.body[key], rule[key] || isHtmlParam(key));
    });
  }
}

/**
 * 重置参数
 * @param  {String} data [description]
 * @param  {String} rule [description]
 * @return {String}      [description]
 */
function rebuildParam(data, rule) {
  if (!data) return data;
  // Check array
  if (Array.isArray(data)) {
    return data.map(function (item) {
      return rebuildParam(item);
    });
  }
  switch (typeof data) {
    case 'object': {
      Object.keys(data).forEach((key) => {
        data[key] = rebuildParam(data[key]);
      });
    }
    break;
  case 'string': {
    // 重设规则匹配
    let ruleMatch = 'normal';
    if (rule === 'close') ruleMatch = 'close';
    else if (rule) ruleMatch = 'xss';
    switch (ruleMatch) {
      case 'xss':
        return filterHtml(data);
      case 'close':
        return data;
      default:
        return escapeParams(data).value;
    }
  }
  }
  return data;
}

/**
 * XSS过滤
 * @param  {Object}   req  请求对象
 * @param  {Object}   res  返回对象
 * @param  {Function} next 下一步
 */
module.exports = function (req, res, next) {
  //过滤路由参数params
  if (req.url) {
    const pathname = url.parse(req.url).pathname; //获取req.url的路径,不包含query
    const decodePathname = decodeURI(pathname); //解码pathname
    const escapedParams = escapeParams(decodePathname); //转义字符
    if (escapedParams.isMatched) {
      let securityError = new Error('验证错误');
      securityError.status = 110;
      next(securityError);
      return;
    }
    req.url = encodeURI(escapedParams.value);
  }
  // 获取当前请求地址
  const curPath = req.originalUrl.split('?')[0].replace(/\/$/, '');
  const method = req.method.toLowerCase();
  let xssRule;
  // 检测在xss配置中是否有特例method
  if (xssConfig[method + ':' + curPath]) {
    xssRule = xssConfig[method + ':' + curPath];
  }
  // 检测是否有通用配置
  if (!xssRule && xssConfig[curPath]) {
    xssRule = xssConfig[curPath];
  }
  // 如果检测到关闭规则,则直接关闭检测
  if (xssRule === 'close') return next();
  // 重设请求对象
  rebuildReq(req, xssRule || {});
  return next();
};