XSS 攻击案例

今天,我们来谈谈 XSS 攻击。

XSS 是什么

XSS 攻击指的是攻击者通过在受信任的网站上注入恶意的脚本,使得用户的浏览器在访问该网站时执行这些恶意脚本,从而导致信息泄露等安全问题。

XSS 英文名 Cross-Site-Scripting,即跨站脚本 。为什么不叫 CSS 呢?因为 CSS 的缩写已经被 Cascading Style Sheets,即层叠样式表占用。

XSS 分类和演示

XSS 攻击主要分成三类:DOMXSS 攻击、反射型 XSS 攻击和存储型 XSS 攻击。

我们接下来需要演示下 XSS 攻击,我们做点前期准备。

案例的演示环境:

macOS Monterey - Apple M1

node version - v14.18.1

Visual Studio Code 及其 Live Server 插件

首先,我们添加个 hostname, 方便测试,当然你可以直接使用 ip 地址测试。

通过 sudo vim /etc/hosts 添加 127.0.0.1 a.example.com 的映射:

本文所有的案例通过 SSR 应用进行演示。

DOMXSS 攻击

DOMXSS 攻击利用了前端 Javascript 在浏览器中动态操作 DOM 的特性DOMXSS 攻击的原理是攻击者通过注入恶意代码或者脚本到网页中的 DOM 元素中,然后通过浏览器执行这些恶意的代码。

案例,如下:

javascript 复制代码
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
const router = new Router();

app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);

router.get('/', async (ctx) => {
  await ctx.render('index', {
    xss: '<script>alert("XSS")</script>',
    content: 'DOM - XSS Attack'
  })
});

app.use(router.routes());
app.listen(3000, () => {
  console.log("listening on http://localhost:3000");
})

上面👆,我们渲染的模版(如下)index,并将数据 xsscontent 传递给模版。

html 复制代码
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DOM - XSS - Jimmy</title>
</head>
<body>
  <h3 style="text-align: center;"><%= content %></h3>
  <%- xss %>
</body>
</html>

在模版中,我们读取了字符串 content,和 html 数据 xss。运行之后,会弹出攻击成功的提示:

反射型 XSS 攻击

反射型 XSS 攻击,指攻击者通过构造恶意的 URL,利用用户的输入参数将恶意的代码注入到目标站点的响应内容中,然后将注入的恶意代码发送给浏览器执行,从而实现攻击。简而言之:就是把用户输入的数据从服务端反射给用户浏览器。

下面是一个小案例:

javascript 复制代码
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
const router = new Router();

app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);

router.get('/', async (ctx) => {
  await ctx.render('index')
});

router.get('/api/username', async (ctx) => {
  let url = ctx.request.url;
  let _username = '';
  if(url.split('username=')[1]) {
    _username = url.split('username=')[1];
  }
  ctx.body = {
    username: _username // <img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> Jimmy
  }
})

app.use(router.routes());
app.listen(3000, () => {
  console.log("listening on http://localhost:3000");
})

上面我们渲染了首页 index 模版,然后提供了一个 /api/username 的接口,返回 username 数据。

首页模版如下:

index.html 复制代码
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>反射型 - XSS - Jimmy</title>
</head>
<body>
  <h3 style="text-align: center;">反射型 - XSS</h3>
  <textarea type="text" id="input" placeholder="please enter username" rows="8"></textarea>
  <button id="trigger">Say Hi</button>
  <p style="color: #f00;" id="hint"></p>
  <script>
    let inputDom = document.getElementById("input");
    let triggerDom = document.getElementById("trigger");
    let hintDom = document.getElementById("hint");
    triggerDom.addEventListener("click", () => {
      const params = {
        username: inputDom.value
      }
      const queryString = Object.keys(params)
        .map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
        .join("&"); 
      
      fetch(`/api/username?${queryString}`, {
        method: "GET"
      })
        .then(response => response.json())
        .then(data => {
          let _username = data.username;
          hintDom.innerHTML = `Hello, ${ decodeURIComponent(_username) }.`;
        })
    })
  </script>
</body>
</html>

我们提供了一个输入框 textarea,然后触发按钮,调用接口获取返回的数据,然后在页面中展示 username。比如,Jimmy 写入输入框,会在页面展示 Hello, Jimmy. 的效果。但是,对于 Hacker 来说,我可以输入:

css 复制代码
<img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> Jimmy

页面的展示内容还是 Hello, Jimmy.,却多做了弹窗的动作。

注意⚠️ 现代浏览器通常会自动阻止通过 innerHTML 插入的包含脚本的内容

储存型 XSS 攻击

存储型攻击,指攻击者利用它在目标站点上储存 的恶意脚本,当用户访问该页面时,恶意脚本被执行。该类攻击在评论区常见。

一般的攻击步骤:

  • hacker 在评论区输入了攻击的脚本
  • 服务度端存储了该脚本
  • 用户浏览网页,拉取了该脚本
  • 脚本执行,攻击生效
javascript 复制代码
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
const router = new Router();

const dataList = [{
  name: 'Ivy',
  comment: 'Nice Day!'
}]

app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);

router.get('/', async (ctx) => {
  await ctx.render('index')
});

router.get('/api/comments', async (ctx) => {
  ctx.body = {
    list: dataList
  }
})

router.get('/api/comment/add', async (ctx) => {
  let query = ctx.request.url.split('?')[1];
  let usernamePart = query.split('&')[0];
  let commentPart = query.split('&')[1];
  dataList.push({
    name: usernamePart.split('=')[1],
    comment: commentPart.split('=')[1]
  })
  ctx.body = {
    message: 'ok' 
  }
})

app.use(router.routes());
app.listen(3000, () => {
  console.log("listening on http://localhost:3000");
})

我们通过变量 dataList 来模拟从数据库数据。接口 /api/comment/add 是添加评论,接口 /api/comments 是拉取评论,读取 dataList 变量值。

模版的设置如下:

html 复制代码
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>存储型 - XSS - Jimmy</title>
</head>
<body>
  <h3 style="text-align: center;">存储型 - XSS</h3>
  <textarea type="text" id="input" placeholder="comment here..." rows="8"></textarea>
  <button id="trigger">Comment</button>
  <ul id="list"></ul>
  <script>
    let inputDom = document.getElementById("input");
    let triggerDom = document.getElementById("trigger");
    let listDom = document.getElementById("list");

    initList();
    triggerDom.addEventListener("click", () => {
      const params = {
        username: 'Jimmy',
        comment: inputDom.value // <img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> XSS happen.
      }
      const queryString = Object.keys(params)
        .map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
        .join("&"); 
      
      fetch(`/api/comment/add?${queryString}`, {
        method: "GET"
      })
        .then(response => response.json())
        .then(data => {
          if(data.message === 'ok') {
            initList();
          }
        })
    })

    function initList() {
      fetch('/api/comments')
        .then(response => response.json())
        .then(data => {
          let list = data.list;
          let domLis = ``;
          for(let i = 0; i < list.length; i += 1) {
            let person = list[i];
            domLis += `<li><b>${ person.name }</b> said: ${ decodeURIComponent(person.comment) }</li>`
          }
          listDom.innerHTML = domLis;
        })
    }
  </script>
</body>
</html>

我们一进来页面,默认拉取了评论列表。触发按钮,添加评论,当评论添加成功后,重新拉取评论列表数据。

XSS 避免

那么,我们应该如何避免 XSS 攻击呢?

  • 输入验证和过滤 :用户输入的内容不能相信,要对用户输入的数据进行验证,只接受可信任的数据。比如对脚本标签 script 处理,剔除该标签的潜在危险
  • 使用安全的框架或者库 :比如选择前端开发框架 Angular,其内置了安全机制,默认 XSS 防护;又比如你可以使用库 xss 来避免此类攻击
  • 设置 HTTP 头部 :我们可以设置适当的 HTTP 头部,比如 Content-Security-Policy (CSP) 内容安全策略X-XSS-Protection等。增强应用程序的安全性。

CSP 比如:

bash 复制代码
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src data: https://images.example.com; font-src 'self' https://fonts.gstatic.com;

default-src 指定了默认的加载源限制为智能从同源地址加载。script-src 指定了可以从同源地址和 https://cdn.example.com 地址加载脚本。后面的 imgfont 类推。

当然,定期更新和修补漏洞也是不可少的。减少给 Hacker 攻击的机会。

参考

相关推荐
mCell24 分钟前
GSAP 入门指南
前端·javascript·动效
gnip1 小时前
组件循环引用依赖问题处理
前端·javascript
方圆想当图灵1 小时前
如何让百万 QPS 下的服务更高效?
分布式·后端
凤山老林2 小时前
SpringBoot 轻量级一站式日志可视化与JVM监控
jvm·spring boot·后端
凡梦千华2 小时前
Django时区感知
后端·python·django
Aotman_2 小时前
el-input textarea 禁止输入中文字符,@input特殊字符实时替换,光标位置保持不变
前端·javascript·vue.js·前端框架·es6
Nan_Shu_6142 小时前
Web前端面试题(1)
前端·面试·职场和发展
EveryPossible2 小时前
选择数据展示
javascript
lypzcgf2 小时前
Coze源码分析-资源库-创建知识库-前端源码-核心组件
前端·typescript·react·coze·coze源码分析·ai应用平台·agent开发平台
Chan162 小时前
JVM从入门到实战:从字节码组成、类生命周期到双亲委派及打破双亲委派机制
java·jvm·spring boot·后端·intellij-idea