跨站脚本攻击(Cross Site Scripting),缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
2. XSS的原理
者对含有漏洞的服务器发起XSS攻击(注入JS代码)。
诱使受害者打开受到攻击的服务器URL。
受害者在Web浏览器中打开URL,恶意脚本执行。
3.防御方式
这里采用的是文本过滤的一种方式来防御XSS,这也是现在我比较常用的。
3.1首先定义一个配置文件,规定哪些请求需要进行文本过滤,如:
{
"xss": {
"post:/api/test/": {
"content": "close"
}
}
}
3.2引入此配置文件,请求时,查看当前请求是否在进行文本过滤的列表里,如果是,将当前参数里面的敏感字符、带有html字符的进行转义,并重新生成参数。
'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();
};