XSS 攻击案例

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

XSS 是什么

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

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

XSS_攻击流程.png

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 的映射:

hosts添加映射.png

本文所有的案例通过 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 传递给模版。

代码语言:javascript
复制
<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_attack_dom.gif
反射型 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 数据。

首页模版如下:

代码语言:javascript
复制
<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: &#34;GET&#34;
  })
    .then(response =&gt; response.json())
    .then(data =&gt; {
      let _username = data.username;
      hintDom.innerHTML = `Hello, ${ decodeURIComponent(_username) }.`;
    })
})

</script>
</body>
</html>

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

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

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

xss_attack_reflect.gif

注意⚠️ 现代浏览器通常会自动阻止通过 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 变量值。

模版的设置如下:

代码语言:javascript
复制
<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(&#34;click&#34;, () =&gt; {
  const params = {
    username: &#39;Jimmy&#39;,
    comment: inputDom.value // &lt;img src=&#34;XX&#34; onerror=&#39;alert(&#34;XSS&#34;)&#39; style=&#34;display: inline-block; width: 0; height: 0;&#34;/&gt; XSS happen.
  }
  const queryString = Object.keys(params)
    .map(key =&gt; encodeURIComponent(key) + &#34;=&#34; + encodeURIComponent(params[key]))
    .join(&#34;&amp;&#34;); 
  
  fetch(`/api/comment/add?${queryString}`, {
    method: &#34;GET&#34;
  })
    .then(response =&gt; response.json())
    .then(data =&gt; {
      if(data.message === &#39;ok&#39;) {
        initList();
      }
    })
})

function initList() {
  fetch(&#39;/api/comments&#39;)
    .then(response =&gt; response.json())
    .then(data =&gt; {
      let list = data.list;
      let domLis = ``;
      for(let i = 0; i &lt; list.length; i += 1) {
        let person = list[i];
        domLis += `&lt;li&gt;&lt;b&gt;${ person.name }&lt;/b&gt; said: ${ decodeURIComponent(person.comment) }&lt;/li&gt;`
      }
      listDom.innerHTML = domLis;
    })
}

</script>
</body>
</html>

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

xss_attack_store.gif

XSS 避免

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

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

CSP 比如:

代码语言:javascript
复制
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 攻击的机会。

参考

  • Cross-site scripting(跨站脚本攻击)
  • Figma
  • xss
  • ejs