记录一次防止js注入的项目经历,起因,项目在测试过程中,发现可能存在注入可能,于是拿到代码开始查看,因为现在前后端分离的情况下,需要特殊处理避免XSS攻击(跨站脚本攻击)的情况已经很少了,所以这次情况引起了我的兴趣。
什么是javascript注入攻击?简单来说,就是页面上输入的内容中带有可执行的javascript,而你使用这段输入内容的时候,让这段用户提供的代码
执行了,也就是你写的代码,执行了非你写的代码,就会导致网页的不可行乃至更严重的安全威胁。
因为传统页面基本都是由服务端进行渲染,也就是说到达浏览的时期的时候,已经是完整的
的页面,包括原本页面的数据,用户输入的数据,都已经在html中了,这意味着什么?如果用户输入了这样一段字符串,而当后端渲染页面未处理的时候,就会渲染一个这样的html:
Document
用户留言列表:
- 用户1:哈哈 我抢到沙发了
- 用户2:不错不错 我是二楼
- 用户3:哈哈 三楼也还可以
- 用户4:
- 用户5:五楼不开心
如果感兴趣可以复制这段代码打开试试!
也就是说,在传统服务端渲染的时候,如果用户输入这段代码,而且我们没有处理,直接想对待正常的字符串一样,直接渲染,那么就完蛋了,所有访问这个页面的人都会看见这个弹框,更有甚着,如果利用这种方式,盗取一些用户信息,cookies之类的发给其余网站,那可能导致用户信息财产的损失…
大家可能注意到了,我在描述这个问题的时候,加上了传统这两个字,大家可能会说现在的服务端渲染依然存在并且不可或缺,我这里的使用传统主要是为了区分现在的前后端分离的形式和服务端渲染的形式。
并且,现在各种服务端渲染的情况,都会进行转义防止js注入。
当我们使用前后端分离的形式进行渲染的时候,和上面服务端直接渲染的时候不一样。
到达浏览器的时候:
Document
用户留言列表:
然后我们通过ajax获取信息列表:
var a={
success: 1,
data: ['用户1:哈哈 我抢到沙发了', '用户2:不错不错 我是二楼', '用户3:哈哈 三楼也还可以', '用户4:','用户5:五楼不开心']
},html="";
a.data.forEach(function(value){
html+= ""+value+" "
})
document.getElementById('mul').innerHTML = html;
拿到这段数据,我们通过for循环,讲数据遍历加上加入页面,神奇的事情发生了。
a.data.forEach(function(value){
var li = document.createElement('li');
li.appendChild(document.createTextNode(value))
document.getElementById('mul').appendChild(li);
})
总结下来:传统方式会产生的问题,在前后端分离中已经降低最小了,指挥影响页面的显示。但是需要注意的是你接受的这段代码,不仅是自己在使用还会被后台处理,存储。如果你不处理,后台也不处理,那么后台都可能被攻击到,所以还是需要处理一下。
当然,现在http请求库中,基本都有相关api进行处理,甚至默认处理了,所以正常情况不会产生这种问题,而且后端也会进行相应的处理和校验。
根据这个原理,提供两个前端转义函数:
//转义 元素的innerHTML内容即为转义后的字符
function htmlEncode(str) {
var ele = document.createElement('span');
ele.appendChild(document.createTextNode(str));
return ele.innerHTML;
}
//解析
function htmlDecode(str) {
var ele = document.createElement('span');
ele.innerHTML = str;
return ele.textContent;
}
我是一个不需要跳转的链接
这种方式来避免a链接的跳转。
那么来详细分析一下实现过来,在点击的时候,识别到javascript:
开头,就会当做js执行执行,void(expression)
,void
是一个操作符,他会计算后面表达式的值但是不会返回结果,所以将a链接的默认跳转转化为了执行一个不会返回内容的js代码,实现了a链接不跳转的效果。
说到这里大家可能就明白了,我们还可以这样
单此处提交表单
这样
执行函数
所以如果我们使用动态插入的链接的时候,如果链接的信息的来源不可信任的情况,例如用户输入,根据用户输入拼接成的链接的情况下,我们就需要校验一下,避免向所有用户推送的内容含有可执行的代码。
2. iframe的src属性,和1中a链接的情况类似。也是我们这次遇到的问题,因为维护一个比较老的项目,整个项目通过iframe实现router
的效果,而链接的来源是通过后台管理系统中配置(手动输入),所以程序扫描结果显示如果在后台输入中输入了可以执行的js代码,就会产生问题。实际上后台管理配置的是开发者。
对于1,2的处理方式,和插入页面显示的内容可以通过转义处理不同,这里我们如果使用转义,就会导致正常的链接也识别不出来,所以只能通过校验之后使用:
方案一:
校验是否是http(s)链接:
function checkURL(URL) {
var str = URL;
var Expression = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
var objExp = new RegExp(Expression);
if (objExp.test(str) == true) {
return true;
} else {
return false;
}
}
方案二:
因为我们发现,如果在链接位置的字符串不是以javascript:
开头的,就不会当成执行语句执行了。
执行函数
所以直接将所有的链接统一转化绝对路径,然后插入,只有可能会导致内容显示不出来,但是不会造成更大危害(如果碰到,肯定会影响用户体验)。
因为是链接,所以会集中情况完整链接,绝对路径,相对路径的链接,我们统一处理成完整链接的形式。
function makeAbsUrl(url) {
if (url.indexOf("//") === 0 ||
url.indexOf("http://") === 0 ||
url.indexOf("https://") === 0) {
return url;
} else {
var el = document.createElement("div");
if (!(url.charAt(0) == '.' || url.charAt(0) == '/')) {
url = "/" + url
}
el.innerHTML = "";
return el.getElementsByTagName("a")[0].href;
}
}
到这里我们基本解决了有程序扫描得到的被js注入的漏洞。
对于这种问题,最好的情况是从源头开始校验,而不是在使用的地方。通过我们使用a链接和iframe的地方,链接的来源都是后台或者写死的,所以是可信的,不会出现这种漏洞。而我们这次项目主要是配置的也是开发者使用的,但是这同样也属于用户操作
所以出现这种漏洞。
同样的情况,也需要确保我们通过ajax发送的内容,转义之后发送给后台,避免因为用户输入的代码对后台服务器和数据库产生危害。
还有一些直接上网址栏输入javascript:alert(1)
和使用控制台修改页面和js代码的时候(大多数淘宝工具浏览器插件就使用类似这种方式),但是这种修改刷新之后就会消失,并且只会影响他修改的那个页面不会发散到所有用户。
请不要信任你的JavaScript,记得偶然的刷到的一个视频,如果在百度上复制资料需要注册或者登陆的时候,打开F12将$=null
就可以复制了,所以任何一个会使用控制台的用户都可能会篡改你的代码。即使只能修改他当前的页面。