其实, 前端的安全并没有很多, 不过知道了, 起码后端兄弟不会那么累了。
本文主要讨论以下几种攻击方式 :
XSS方式
CSRF方式
点击劫持
希望大家在阅读完文本之后, 能够很好地回答以下的几个问题:
前端的攻击方式有哪些?
什么是XSS方式? XSS攻击有几种类型?如何防范XSS攻击?
什么是CSRF攻击?如何防范CSRF攻击?
如何检测网站是否安全?
- XSS方式
XSS(Cross-Site-Scripting),跨站脚本攻击是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登录网站时就会执行这些恶意代码, 这些脚本可以读取 cookie ,session tokens , 或者其他敏感的网站消息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。
XSS的本质:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,利用这些信息冒充用户向网站发起攻击
XSS的分类:
存储型
反射型
DOM 型
1.1 反射型XSS
当用户点击一个恶意链接, 或者提交一个表单, 或者进入一个恶意网站时, 注入脚本进入被攻击者的网站。Web 服务器将注入脚本,比如一个错误信息, 搜索结果等 , 未进行过滤直接返回到用户的浏览器上。
反射型XSS的攻击步骤:
1.攻击者构造出特殊的URL, 其中包含恶意代码
2.用户打开带有恶意代码的URL时, 网站服务端将恶意代码从 URL中取出, 拼接在 HTML中返回给浏览器
3.用户浏览器接收到响应后解析执行, 混在其中的恶意代码也被执行。
4.恶意代码窃取用户数据并发送到攻击者的网站, 或者冒充用户的行为, 调用目标网站接口执行攻击者指定的操作。
反射型 XSS 漏洞常见于 URL 传递参数的功能 , 如网站搜索 , 跳转等。由于需要用户主动打开恶意的 URL 才能生效 , 攻击者往往会结合多种手段诱导用户点击
POST 的内容也可以触发反射型 XSS , 只不过其触发条件比较苛刻 (需要构造表单提交页面 , 并引导用户点击),所以非常少见。
话不多说 , 我们来举个例子
//前端代码
// index.html
// 由于必须诱导用户点击,所以可能是这样的
//这是一张很好看的图片
// 后端代码
// server.js
const express = require(‘express’);
const app = express();
app.use(express.static(path.join(__dirname)))
app.get(’/welcome’,function(req, res) {
// 把恶意代码当做字符串, 传输回浏览器
res.send(${req.query.type}
);
res.end();
})
app.listen(3000, ()=> {
console.log(‘server is running at port 4000’)
});
复制代码
如果不希望被前端拿到 cookie, 后端可以设置 httpOnly (不过这个不是 XSS 的解决方案 , 只能降低受损范围)
如何防范反射型 XSS攻击?
对字符串进行编码
对url的查询参数进行转义后再输出到页面
app.get(’/welcome’,function(req,res) {
//对查询参数进行编码,避免反射型 XSS攻击
res.send(${encodeURIComponent(req.query.type)}
);
})
复制代码
总结:简单来说,前端向后端发送GET请求数据,后端返回结果之前,必须先对url查询参数进行编码再输出到页面
1.2 DOM型XSS
DOM型 XSS 攻击 , 实际上就是前端 javaScript 代码不够严谨 , 把不可信的内容插入到了页面 。在使用 .innerHTML , .outerHTML , appendChild , document.write()等 API 时要特别小心 , 不要把不可信的数据作为为 HTML 插入到页面上 , 尽量使用 .innerText , .textContent , setAttribute()等。
DOM型XSS 的攻击步骤:
1.攻击者构造出特殊数据 , 其中包含恶意代码
2.用户浏览器执行恶意代码
恶意代码窃取用户数据并发送到攻击者的网站 , 或者冒充用户的行为 , 调用目标网站接口执行攻击者指定的操作。
如何防范 DOM型XSS攻击
防范 DOM 型 XSS攻击的核心就是对输入内容进行转义 (DOM 中的内联事件监听器和链接跳转都能把字符串作为代码运行, 需要对其内容进行检查)
对于 url 链接(例如图片的src属性), 那么直接使用 encodeURIComponent来转义
非 url , 我们可以这样进行编码
function encodeHtml(str) {
return str.replace(/"/g, ‘"’)
.replace(/’/g, ‘’’)
.replace(/ .replace(/>/g, ‘>’);
}
复制代码
DOM型 XSS攻击中 , 取出和执行恶意代码由浏览器端完成 , 属于前端 javaScript 自身的安全漏洞
再举个例子
//前端
复制代码 总结:就是不要相信用户输入的内容,如果非要在页面上插入 HTML,就必须先转义再插入。对于URL可以使用encodeURIComponent,对于非URL可以使用上面的方法 1.3 存储型 XSS 恶意脚本永久存储在目标服务器上。当浏览器请求数据时, 脚本从服务器传回并执行,影响范围比反射型和 DOM型 XSS更大。 存储型XSS攻击的原因仍然是没有做好数据过滤;
前端提交数据到服务端时 , 没有做好过滤;
服务端在接受到数据时 , 在存储之前 , 没有做好过滤
前端从服务端请求到数据 , 没有过滤输出。
存储型XSS的攻击步骤
1.攻击者将恶意代码提交到目标网站的数据库中
2.用户打开目标网站时 , 网站服务端将恶意代码从数据库取出, 拼接在 HTML中返回给浏览器
3.用户浏览器接收到响应后解析执行, 混在其中的恶意代码也被执行。
4.恶意代码窃取用户数据并发送到攻击者的网站, 或者冒充用户的行为, 调用目标网站接口执行攻击者指定的操作
这种攻击常见于带有用户保存数据的网站功能, 如论坛发帖, 商品评论, 用于私信等。
如何防范存储型 XSS攻击
1.前端数据传递给服务器之前, 先转义/过滤(防范不了抓包修改数据的情况)
2.服务端接收到数据, 在存储到数据库之前, 进行转义/过滤
3.前端接收到服务器传递过来的数据, 在展示到页面前, 先进行转义/过滤
来一个例子, 可能长一点 , 登录用了一下 cookie
// 前端
// index.html
论坛
});
//前端 login.html
登录
// 后端 server.js
const express = require(‘express’);
const app = express();
const path = require(‘path’);
const bodyParser = require(‘body-parser’);
const cookieParser = require(‘cookie-parser’);
//设置路径
app.use(express.static(path.join(__dirname, ‘src’)));
app.use(express.static(path.join(__dirname, ‘…/’)));
//将参数转换成对象
app.use(bodyParser.urlencoded({ extended: true }));
//req.cookie[xxx] 获取cookie
app.use(cookieParser());
//用户列表
let userList = [{ username: ‘zs’, password: ‘123456’ }, { username: ‘star’, password: ‘star’ }];
let SESSION_ID = ‘connect.sid’;
let session = {};
//登录接口
app.post(’/api/login’, (req, res) => {
let { username, password } = req.body;
let user = userList.find(item => item.username === username && item.password === password);
if (user) {
//用户登录后,给一个标识(cookie登录)
const cardId = Math.random() + Date.now();
session[cardId] = { user };
res.cookie(SESSION_ID, cardId);
res.json({ code: 0 });
} else {
res.json({ code: 1, error: ${username} does not exist or password mismatch
});
}
});
//1.反射型XSS攻击: http://localhost:3000/error?type=
app.get(’/error’, function (req, res) {
res.send(${req.query.type}
); //拿到 url 上的 type 参数,并返回给前端
});
app.get(’/welcome’, function (req, res) {
//对查询参数进行编码,避免XSS攻击
res.send(${encodeURIComponent(req.query.type)}
);
//对type查询参数进行编码,即可解决当前的XSS攻击(可重启服务查看)
// res.send(${encodeURIComponent(req.query.type)}
);
});
//安全的评论列表
let comments2 = [
{ username: ‘zs’, content: ‘我是zs’ },
{ username: ‘hw’, content: ‘我是hw’ },
{ username: ‘star’, content: ‘大家好,我是Star’ },
]
app.get(’/getComments2’, function (req, res) {
res.json({ code: 0, comments: comments2 });
});
function encodeHtml(str) {
return str.replace(/"/g, ‘"’)
.replace(/’/g, ‘’’)
.replace(/ .replace(/>/g, ‘>’);
}
app.post(’/addComment2’, function (req, res) {
//cardId (req.cookies[SESSION_ID])要派上用场啦~
let info = session[req.cookies[SESSION_ID]];
if (info) {
//用户已经登录
let username = info.user.username;
// 服务器接收到数据后 , 在存储到数据库之前 , 进行转义/过滤
comments2.push({ username, content: encodeHtml(req.body.comment) });
res.json({ code: 0, comments: comments2 });
} else {
res.json({ code: 1, error: ‘user not logged in.’ });
}
});
app.listen(3000);
复制代码
总结:
1.恶意脚本如果未经过转换, 存储到了后台。任何用户访问此页面, 都会执行恶意脚本
2.说白了就是增加字符串的过滤:
前端输入时过滤
服务端增加时过滤
前端输出时过滤
1.4 JSONP中存在的 XSS 安全问题
//一个 jsonp函数大概是这样
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement(‘script’);
params = JSON.parse(JSON.stringify(params));
let arrs = [];
for (let key in params) {
arrs.push(${key}=${params[key]}
);
}
arrs.push(callback=${callback}
);
script.src = ${url}?${arrs.join('&')}
;
document.body.appendChild(script);
console.log(callback)
window[callback] = function (data) {
resolve(data);
document.body.removeChild(script);
}
})
}
//如果我这样调用 ,就会 XSS安全问题
jsonp({
url: ‘http://localhost:3000/say’,
params: {
wd: ‘I Love you’
},
callback: ‘alert(1)’
}).then(data => {
alert(data.username)
console.log(data)
})
复制代码
简单来说就是把 jsonp中回调函数的参数设置为恶意代码
常见的预防操作
将重要的cookie标记为http only,这样的话Javascript 中的document.cookie语句就不能获取到cookie了。
只允许用户输入我们期望的回调参数
let { callback} = req.query;
if(callback === ‘show’) {
callback = ‘show’;
} else {
res.end(‘error’)
}
复制代码
过滤或移除特殊的Html标签
//safe.html 显示页面
转账
用户:
余额:
钓鱼连接
//钓鱼页面
你的钱不安全了~
返回查看余额
//恶意站点
// fake1.html
//后端 server1.js
const express = require(‘express’);
const app = express();
const path = require(‘path’);
const bodyParser = require(‘body-parser’);
const cookieParser = require(‘cookie-parser’);
//将参数转换成对象
app.use(bodyParser.urlencoded({ extended: true }));
//req.cookie[xxx] 获取cookie
app.use(cookieParser());
//设置路径
app.use(express.static(path.join(__dirname, ‘src’)));
//用户列表
let userList = [{ username: ‘hw’, password: ‘123456’, account: 1000 }, { username: ‘loki’, password: ‘loki’, account: 100000 }];
let SESSION_ID = ‘connect.sid’;
let session = {};
//登录接口
app.post(’/api/login’, (req, res) => {
let { username, password } = req.body;
let user = userList.find(item => item.username === username && item.password === password);
if (user) {
//用户登录后,给一个标识(cookie登录)
const cardId = Math.random() + Date.now();
session[cardId] = { user };
res.cookie(SESSION_ID, cardId);
res.json({ code: 0 });
} else {
res.json({ code: 1, error: ${username} does not exist or password mismatch
});
}
});
//获取信息
app.get(’/api/userinfo’, (req, res) => {
let info = session[req.cookies[SESSION_ID]];
if (info) {
//用户已经登录
let username = info.user.username;
res.json({ code: 0, info: { username : username, account: info.user.account} });
} else {
res.json({ code: 1, error: ‘user not logged in.’ });
}
})
//转账前, 先验证 token
app.post(’/api/transfer3’, (req, res) => {
let info = session[req.cookies[SESSION_ID]];
if(info) {
// 用户已经登录
let { payee , amount , token } = req.body;
console.log(token);
console.log(‘my_token_’ + req.cookies[SESSION_ID]);
// 校验 token
if(token === ‘my_token_’ + req.cookies[SESSION_ID] && Number(amount)) {
// token 正确
let username = info.user.username;
userList.forEach(user => {
if(user.username === username) {
user.account -= amount;
}
if(user.username === payee) {
user.account += amount;
}
});
res.json({ code : 0})
}
} else {
res.json({code : 1, error : ‘user not logged in’})
}
})
app.listen(3001, () => {
console.log(‘Server is running at port 3001’)
})
//后端 server2.js
const express = require(‘express’);
const app = express();
const path = require(‘path’);
const bodyParser = require(‘body-parser’);
const cookieParser = require(‘cookie-parser’);
//将参数转换成对象
app.use(bodyParser.urlencoded({ extended: true }));
//req.cookie[xxx] 获取cookie
app.use(cookieParser());
//设置路径
app.use(express.static(path.join(__dirname, ‘dest’)));
app.listen(3002, () => {
console.log(‘Server is running at port 3002’)
})
复制代码
总结:
在登录之后,进入 safe.html 转账页面
在转账页面如果点击 钓鱼连接, 将被攻击, 第三方网站将由于携带 用户cookie, 所有可以绕开验证, 冒用被攻击者的身份, 做一些攻击者指定的操作。
4.Samesite Cookie属性
为了从源头上解决这个问题, Google 起草了一份草案来改进 HTTP协议 , 为 Set-Cookie 响应头新增 Samesite 属性 , 它用来表明这个 Cookie 是个 “同站Cookie” , 同站 Cookie只能作为第一方 Cookie, 不能作为第三方 Cookie, Samesite 有两个属性值 , 分别是 Strict 和 Lax。
部署简单, 并能有效防御 CSRF 攻击 , 但是存在兼容性问题
Samesite=Strict
Samesite=Strict 被成为是严格模式 , 表明这个 Cookie 在任何情况都不可能作为第三方的 Cookie, 有能力阻止所有 CSRF攻击。此时 , 我们在B 站点下发起对 A 站点的任何请求, A站点的 Cookie 都不会包含在 cookie请求头中。