本文将详细阐述浏览器中的安全策略(同源策略、内容安全策略),web漏洞预防(XSS、CSRF、点击劫持),跨域问题 及 浏览器架构中的安全策略等
前面介绍《浏览器架构》时提起过,由于JS引擎及渲染引擎容易出现异常,所有浏览器有多个渲染进程(通常一个tab对应一个渲染进程),且渲染进程是是个沙箱环境。
浏览器本身的漏洞要远比 网页漏洞如XSS危害要大得,堪比“核弹”
思考:
浏览器在处理URL请求之前,会检查渲染进程是否有权限请求该URL,比如
通常情况下,如果你要实现一个UI程序,操作系统会提供一个界面(窗口句柄)给你,该界面允许应用程序与用户交互,允许应用程序在该界面上进行绘制,比如。
同样,渲染进程不能直接访问窗口句柄。所以渲染进程需要完成以下两点大的改变。
所谓的站点隔离是指Chrome将同一站点(包含了相同根域名和相同协议的地址)中相互关联的页面放到同一个渲染进程中执行。
按标签页划分渲染进程的问题
- 最开始 Chrome 划分渲染进程是以标签页为单位,也就是说整个标签页会被划分给某个渲染进程。但是,按照标签页划分渲染进程存在一些问题,原因就是一个标签页中可能包含了多个 iframe,而这些 iframe 又有可能来自于不同的站点,这就导致了多个不同站点中的内容通过 iframe 同时运行在同一个渲染进程中
- 目前所有操作系统都面临着两个 A 级漏洞——幽灵(Spectre)和熔毁(Meltdown),这两个漏洞是由处理器架构导致的,很难修补,黑客通过这两个漏洞可以直接入侵到进程的内部,如果入侵的进程没有安全沙箱的保护,那么黑客还可以发起对操作系统的攻击。 所以如果一个银行站点包含了一个恶意 iframe,然后这个恶意的 iframe 利用这两个 A 级漏洞去入侵渲染进程,那么恶意程序就能读取银行站点渲染进程内的所有内容了,这对于用户来说就存在很大的风险了。
- 因此 Chrome 几年前就开始重构代码,将标签级的渲染进程重构为 iframe 级的渲染进程,然后严格按照同一站点的策略来分配渲染进程,这就是 Chrome 中的站点隔离。
- 实现了站点隔离,就可以将恶意的 iframe 隔离在恶意进程内部,使得它无法继续访问其他iframe 进程的内容,因此也就无法攻击其他站点了。
什么是同源策略?
具体来讲 同源策略主要表现在DOM、Web数据和网络这三个层面
opener.document
操作前者DOM 比如 .document.body.style.display="none"
隐藏前一个页面内容Web Worker
Worker的限制localStorage
sessionStorage
IndexDB
背景
- HTTP是一种无状态的协议,不会保存客户端和服务端的通信状态。
- 但随着Web的发展,HTTP的无状态也出现问题,如登录一家网站,即时跳转了其他网站,依然需要保存登录状态。于是运用了Cookie就可以管理网络状态了。
定义:
Set-Cookie: <cookie-name>=<cookie-value>[;<attribute-name>=<attribute-value>]...
<cookie-name>
和<cookie-value>
是必须的,表示cookie的名称和值,可以包含多个属性。常见的Cookie属性包括:
Expires
过期时间, 如果未指定,则当前窗口关闭Cookie失效Max-Age
最大存活时间(单位秒)这个是相对时间,从接收响应开始计算
0
0
表示立即删除CookieDomain
指定Cookie的域名,子域名可以携带父域名的cookie,反之不行Path
指定Cookie的路径, 子路径可以携带上级路径的cookie,反之不行Secure
如果设置了该属性 仅https写一下才能发送cookieHttpOnly
指定Cookie只能服务端(Set-Cookie
)修改 不能通过JS(document.cookie
、XMLTTPRequest
、Request APIs
)修改Priority
Low
、Medium
(默认值) 、High
SameSite
指定Cookie的同站策略
Strict
、Lax
、None
SameSite=Strict
严格模式 完全禁止第三方Cookie,跨站点时任何情况下都不发送Cookie。这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。SameSite=Lax
大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。SameSite=None
Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。RFC规范对Cookie的要求
- 每个Cookie的总长度,浏览器至少要支持到4KB
- 浏览器对每个域名下的cookie至少要支持50个
- 浏览器总共至少要支持3000个Cookie
优缺点
tsfunction setCookie ({
name = throwError('name'),
value = throwError('value'),
// expires = 24 * 60 * 60 * 1000,
MaxAge = 6, // 秒
path = '/',
domain = location.host
} = throwError('参数')) {
// const exp = new Date();
// exp.setTime(exp.getTime() + expires);
// 写入Cookie, toGMTString将时间转换成字符串
document.cookie = `${name}=${encodeURIComponent(value)};max-age=${MaxAge};path=${path};domain=${domain}`;
}
/**
* cookie中取值
* */
function getCookie (name = throwError('参数')) {
const reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)"); // 匹配字段
const arr = document.cookie.match(reg)
if (arr) {
return decodeURIComponent(arr[2]);
} else {
return null;
}
};
/**
* 清除指定cookie值
* 要删除一个cookie,domain和路径必须完全相同
* */
function delCookie ({
name = throwError('参数'),
path = '/',
domain = location.host
}) {
const cval = getCookie(name);
if (cval && cval != null) {
document.cookie = `${name}=${cval};max-age=0;path=${path};domain=${domain}`
}
};
const throwError = (name) => {
throw new Error(`${name}必填!`)
}
使用第三方cookie 跟踪用户的原理
你曾经在A网站浏览信息时,当访问B网站信息时发现它也知道你曾经购买了什么
直接使用Cookie,等于明文存储用户信息,为了避免用户信息被别人看到,服务端引入了Session机制。
Session存储在服务端:通过cookie存储一个session_id,然后具体的数据则是保存在session中。如果用户已经登录,则服务器会在cookie中保存一个session_id,下次再次请求的时候,会把该session_id携带上来,服务器根据session_id在session库中获取用户的session数据。就能知道该用户到底是谁,以及之前保存的一些状态信息。这种专业术语叫做server side session。
跨域问题是浏览器出于安全问题而设计的同源策略限定的
提示
利用 <script>
标签没有默认没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做配合才可以。
举例:
js// window.cb
function localHandler(res) {
// 这里res即是服务端返回的数据
// 格式如 {name: 'zs', age: 12}
}
// 注意这里 请求的接口是与页面非同源, a.json返回的其实是个js文件
jsonP('http://localhost:8081/a.json', localHandler);
function jsonP(url, cb) {
const script = document.createElement('script');
script.setAttribute('src', `${url}?jsonpCallback=${cb.name}`);
document.body.appendChild(script);
}
/**
* JSONP需要服务端配合
* 1. 响应头类型 content-type: "text/javascript"
* 2. 模拟js函数调用 如 localHandler({})
*/
优缺点:
Cross-Origin Resource Sharing 跨域资源共享, 这个也是比较推荐的做法。
Q:什么是CORS?
1)访问站点A的请求,浏览器应告知该请求来自站点B
2)站点A的响应中,应明确哪些跨域请求是被允许的(站点B是被允许的)
如果A站点请求B站点的资源,A站点必须在HTTP响应中显示的告知浏览器,允许访问B站点的资源
简单请求
简单请求的必要条件
Accept
Accept-language
Content-Language
Content-Type
Content-Type
的值只能是:'text/plain' 'multipart/form-data' 'application/x-www-form-urlencoded' 三者之一
简单请求的执行过程Origin
头部告知来自哪个域Access-Control-Allow-Origin
头部表示允许那些域简单请求以外的其他请求
访问资源前,需要先发起preflight
预检请求(方法为OPTIONS
)访问和中请求是被允许的
具体执行过程如下
请求头部
AJAX
等请求中表明来自某个域名下的脚本,以通过服务器的安全校验响应头部
Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
可控使用,其他的JS脚本无法读取无论简单请求还是复杂请求都需要服务端配合
Access-Control-Allow-Origin使用注意
Access-Control-Allow-Origin
是被动态产生的话,就要用Vary: Origin
去指定。Access-Control-Allow-Origin: *
(允许所有的跨域访问)后,就不能在设置Access-Control-Allow-Credentials: true
也就是cookie会丢失Access-Control-Allow-Origin: https://example1.com https://example2.com
Access-Control-Allow-Origin: *.example1.com
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- message: 将要发送到其他 window的数据。
- targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,目标窗口的协议、主机地址或端口与targetOrigin完全匹配,消息才会被发送。
- transfer(可选) 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
Websocket它实现了浏览器与服务器的全双工通信,它也可以作为跨域问题的一种解决方案, 需要服务器作为消息的中转
跨域问题是浏览器的安全策略, 服务端并没有这个问题,我们可以新建一个符合同源策略的代理服务器,作为处理消息的中转。
类似Node中间件代理,需要待见一个符合同源策略的中转nginx服务器, 用于转发请求和响应
nginx配置示例如下
nginxserver { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; add_header Access-Control-Allow-Credentials true; } }
这是一种比较hack(奇葩)的玩法,利用iframe切换后window.name内容不变,将外域数据传递到本域。
window.name可以支持非常长的name值(2MB)
示例代码如下
html<!-- a,b两个页面同源 localhost:3000 c页面为跨域页面 localhost:4000 -->
<!-- a.html -->
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
} else {
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
<script>
<!-- b.html为中间代理页,与a.html同域,内容为空。-->
<!-- c.html http://localhost:4000/c.html -->
<script>
window.name = '要传输的数据'
</script>
总结: 它却是一种跨域问题的解决方案,同也需要第三方资源配合,但从性能角度考虑比较鸡肋
与 window.name + iframe 方式类似
该方式与 window.name + iframe 方式类似,优于后者, 但缺点是只能用于二级域名相同的情况
document.domain = '相同的一级域名如warmplace.cn'
XSS
全称是 Cross Site Scripting
,为了与“CSS”
区分开来,故简称 XSS
,翻译过来就是“跨站脚本”。XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
常见于留言板、评论等功能中
与存储型XSS攻击类似, 但是Web服务器不会存储反射性XSS攻击的恶意脚本。
如下nodejs案例:
js// xss.js
const http = require('http');
const createHtml = function (q) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
${q}
</body>
</html>
`
}
const server = http.createServer((req, res) => {
const search = req.url.split('#')[0].split('?')[1] || ''
const querys = search.split('&').reduce((sum, it) => {
const [k, v=''] = it.split('=');
sum[k] = decodeURIComponent(v);
return sum;
}, {});
console.log(req.url);
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(createHtml(querys.search))
})
server.listen(3000, () => {
console.log('Server is running on http://localhost:3000')
})
node xss.js
启动基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。
XSS常见于富文本编辑器中,前端需要对富文本中的内容进行转义处理。 转义有多种方式
cheeriojs转义示例代码
jsfunction xssFilter (html) {
if(!html) return ''
const cheerio = require('cheerio')
var $ = cheerio.load(html)
const whiteList = {
'img': ['src'], // 只允许src属性
'font': ['color', 'size'],
'a': ['href']
}
$('*').each((index, elem) => {
if(!whiteList[elem.name]) {
$(elem).remove();
}
for(let attr in elem.attribs) {
if(whiteList[elem.name].indexOf(attr) === -1) {
$(elem).attr(attr, null)
}
}
})
return $.html()
}
自定义规则转义示例代码
jsconst htmlEscapes = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
const reUnescapedHtml = /[&<>"']/g
const reHasUnescapedHtml = RegExp(reUnescapedHtml.source)
function escape(string) {
return (string && reHasUnescapedHtml.test(string))
? string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr])
: string
}
<script>alert('你被 xss 攻击了')</script>
<script>alert(' 你被 xss 攻击了 ')</script>
httpOnly
属性, 防止黑客盗用cookie信息X-XSS-Protection: 1
默认值是1,它只防御 参数出现在html内容或属性
X-Content-Type-Options: nosniff
早期的IE浏览器会根据响应内容推测资源格式,有可能将数据当成程序运行,这个就是不让IE胡乱推测的CSP 有如下几个功能
限制加载其他域下的资源文件,这样即使黑客插入了一个 JavaScript 文件,这个JavaScript 文件也是无法被加载的;
禁止向第三方域提交数据,这样用户数据也不会外泄;
禁止执行内联脚本和未授权的脚本;
还提供了上报机制,这样可以帮助我们尽快发现有哪些 XSS 攻击,以便尽快修复问题。
以禁止加载其他域下的资源为例
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
res.writeHead('Content-Security-Policy', 'default-src \'self\'')
也可以不执行限制选项, 只记录违反限定的行为
html<meta http-equiv="content-security-policy" content="策略">
<meta http-equiv="content-security-policy-report-only" content="策略">
同样支持meta设置和HTTP响应头两种方式
假设example.org网站的CSP策略如下
httpContent-Security-Policy: default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports
则会往http://example.com/_/csp-reports
上报POST请求, 上报内容如下
json{
"csp-report": {
"blocked-uri": "http://example.com/css/style.css",
"disposition": "report",
"document-uri": "http://example.com/signup.html",
"effective-directive": "style-src-elem",
"original-policy": "default-src 'none'; style-src cdn.example.com; report-to /_/csp-reports",
"referrer": "",
"status-code": 200,
"violated-directive": "style-src-elem"
}
}
http// 限制所有的外部资源,只能从当前域名加载 Content-Security-Policy: default-src 'self' // 限制所有的外部资源,只能从同源域名或 信任的子域名加载 Content-Security-Policy: default-src 'self' *.trusted.com // 错误写法,第二个指令将会被忽略 Content-Security-Policy: script-src https://host1.com; script-src https://host2.com // 多个指令用;分割, 细分指令优先, 'none' 表示限制所有来源 Content-Security-Policy: default-src https://host1.com https://host2.com; frame-src 'none'; object-src 'none' // 发送报告 POST请求 Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;
参考资料:
Cross-Site request forgery) 跨站请求伪造
经过上述6个步骤后,攻击者(WebB)就拿到了用户C的cookie,从而访问WebB时冒充用户C的身份,进行一些恶意操作,如下单转账等。
必要条件
从上图可以看出,要完成一次CSRF攻击,受害者必须满足两个必要的条件
html<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>csrf demo</title>
</head>
<body>
hello,欢迎来到钓鱼网站 http://127.0.0.1:5500 。
<script>
// 这里是恶意脚本
document.write(`
<form name="commentForm" target="csrf" method="post" action="http://localhost/post/addComment">
<input name="postId" type="hidden" value="1">
<textarea name="content">来自CSRF!</textarea>
</form>`
);
/**
* 这里加了iframe, form表单请求时就不会跳转
* 当然你也可以手写ajax或fetch请求
*/
var iframe = document.createElement('iframe');
iframe.name = 'csrf';
iframe.style.display = 'none';
document.body.appendChild(iframe);
setTimeout(function(){
// 127.0.0.1:5500 网站是无法拿到 localhost网站的cookie (站点隔离机制)
// 但是可以往 localhost 网站发出请求,会携带localhost的token
// 利用该机制就可以冒充用户做一些事情
document.querySelector('[name=commentForm]').submit();
},1000);
</script>
</body>
</html>
上例演示了攻击者冒用你的身份发表了一次评论
但如果localhost网站 提交请求时get请求,那情况会更坏,攻击者冒用你的身份发表的评论内容是富文本,里面有链接假设链接为 <a href='http://诈骗网站'>点击这里有钱哦!</a>
这个链接还做了一些事情如别人一点又发表一次评论,这时候就会变成一个蠕虫病毒!
当然这种攻击方式还需要用户点击,有没有不需要用户点击的方式呢?有! 假设富文本的内容包含
<img src="http://localhost/ajax/addComment?postId=1&content=浏览就中招,自动传播">
这种情况更更恐怖!!
CSRF 盗用用户身份, 除了发帖外, 还可能盗用用户资金(转账、消费), 最终会损外网站声誉。
<input name="csrftoken" value="csrfTokenValue" type="hidden">
用户凭证如何设计?
Cookies和XSS的关系
Cookies和CSRF的关系
Cookies安全策略
示例代码
html<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>csrf demo</title>
</head>
<body style="background:url(clickhijack.png) no-repeat">
<iframe style="opacity:.1" src="http://localhost:1521/post/1" width="800" height="600"></iframe>
</body>
</html>
点击劫持的特点
危害
js// 在 iframe 中 top.location 和 window.location不相等
if(top.location !== window.location){
top.location = window.location
}
// 这种方式有缺陷,攻击者通过设置iframe属性 sandbox="allow-forms" 禁用脚本
res.writeHead('X-Frame-Options', 'DENY')
ctx.set('X-Frame-Options', 'DENY')
由于HTTP采用明文传输,在网络传输环境其内容很容易被撰改,进而发生
哪些环节会发生内容撰改?
解决方案 HTTPS(TLS/SSL加密), 具体将在我的网络协议篇章讲解
密码的替代方案
密码-泄露渠道
密码泄露案例(2016)
hash算法特征
密码-单向变换彩虹表 (md5在线解密) 如何对抗彩虹表? 用复杂密码、增加运算、多次加密
密码-变换次数越多越安全
生物特征密码隐患 下图是美国国防部长的一次发言,媒体拍摄到,提取了美国国防部长的指纹
密码-生物特征密码
定义:
大规模分布式拒绝服务攻击DDOS
DOS攻击方式
DOS攻击案例(2009)
DOS攻击防御
DOS攻击预防
如何防御
上传问题防御
部署应用时一般要使用权限低的满足网站需要的用户
信息泄露的途径
比如登录时用到的sql select * from table where id = ${id};
假设用户不听话,传了 1 or 1 = 1
这时sql变成 select * from table id = 1 or 1 = 1;
这句话的含义是什么?举例场景例子 红色是用户输入的
手动执行一下
通过错误在页面上回显,这个不听话的用户就盗取了你网站的所有用户信息
sqlselect * from table where id="10" and 1=0;
select * from table where id="10" or 1=1;
select * from table where id="10" and mid(version(),1,1)=5;
select 1,2,3 from table;
select id,1 ,2,3 from table;
select * from table union select 1,2,3 from table2;
select * from table where mid(username,1,1)="t";
在昨日头条点开一篇文章
推测 10 就是文章 ID ,我们探测一下sql能不能注入
控制台输入
jslocation.href = `http://localhost/post/10 and 1=1`
// 或
location.href = `http://localhost/post/10 and 1=0`
发现页面正常显示
加上引号再探测一下
jslocation.href = `http://localhost/post/10" and '1'='1' and ''="` // 页面正常
location.href = `http://localhost/post/10" and '1'='0' and ''="` // 页面不正常
这时候一般就是有sql可以注入的问题
顺藤摸瓜,我们来推一下mysql版本
jslocation.href = `http://localhost/post/10" and mid(version(),1,1)=5 and ''="`
location.href =
http://localhost/post/10" and mid(version(),1,1)=5 and ''="``
看到页面正常,所以得出数据库版本第一位是5同理可以推出第二位
mysql版本是5.7
攻击者就可以往上查询这个版本的数据库有什么漏洞
再通过union语句推测表里面有多少字段及每个字段名称 略
再看下面的脚本
jslocation.href = `http://localhost/post/100" union select 1,username,2,password,5,6 from user where ""="`
此时用户名密码就暴露出来了!
SQL注入的危害
检查数据类型
对数据进行转义
参数化查询
mysql2
cnpm i mysql2 -S
有一个博客系统用mongoole数据库
通过开发者工具找到登录请求
通过修改curl发起请求
如何防御?
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!