WAVE SUMMIT 2021深度学习开发者峰会 点击报名 了解详情
写点什么

浅谈 React 中的 XSS 攻击

2021 年 5 月 03 日

浅谈 React 中的 XSS 攻击

前言


前端一般会面临 XSS 这样的安全风险,但随着 React 等现代前端框架的流行,使我们在平时开发时不用太关注安全问题。以 React 为例,React 从设计层面上就具备了很好的防御 XSS 的能力。本文将以源码角度,看看 React 做了哪些事情来实现这种安全性的。


XSS 攻击是什么


Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。XSS 攻击通常指的是利用网页的漏洞,攻击者通过巧妙的方法注入 XSS 代码到网页,因为浏览器无法分辨哪些脚本是可信的,导致 XSS 脚本被执行。XSS 脚本通常能够窃取用户数据并发送到攻击者的网站,或者冒充用户,调用目标网站接口并执行攻击者指定的操作。


XSS 攻击类型

反射型 XSS


  • XSS 脚本来自当前 HTTP 请求

  • 当服务器在 HTTP 请求中接收数据并将该数据拼接在 HTML 中返回时,例子:


// 某网站具有搜索功能,该功能通过 URL 参数接收用户提供的搜索词:https://xxx.com/search?query=123// 服务器在对此 URL 的响应中回显提供的搜索词:<p>您搜索的是: 123</p>// 如果服务器不对数据进行转义等处理,则攻击者可以构造如下链接进行攻击:https://xxx.com/search?query=<img src="empty.png" onerror ="alert('xss')">// 该 URL 将导致以下响应,并运行 alert('xss'):<p>您搜索的是: <img src="empty.png" onerror ="alert('xss')"></p>// 如果有用户请求攻击者的 URL ,则攻击者提供的脚本将在用户的浏览器中执行。
复制代码


存储型 XSS


  • XSS 脚本来自服务器数据库中

  • 攻击者将恶意代码提交到目标网站的数据库中,普通用户访问网站时服务器将恶意代码返回,浏览器默认执行,例子:


// 某个评论页,能查看用户评论。// 攻击者将恶意代码当做评论提交,服务器没对数据进行转义等处理// 评论输入:<textarea>  <img src="empty.png" onerror ="alert('xss')"></textarea>// 则攻击者提供的脚本将在所有访问该评论页的用户浏览器执行
复制代码


DOM 型 XSS


该漏洞存在于客户端代码,与服务器无关


  • 类似反射型,区别在于 DOM 型 XSS 并不会和后台进行交互,前端直接将 URL 中的数据不做处理并动态插入到 HTML 中,是纯粹的前端安全问题,要做防御也只能在客户端上进行防御。


React 如何防止 XSS 攻击


无论使用哪种攻击方式,其本质就是将恶意代码注入到应用中,浏览器去默认执行。React 官方中提到了 React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串,因此恶意代码无法成功注入,从而有效地防止了 XSS 攻击。我们具体看下:


自动转义


React 在渲染 HTML 内容和渲染 DOM 属性时都会将 "'&<> 这几个字符进行转义,转义部分源码如下:


for (index = match.index; index < str.length; index++) {  switch (str.charCodeAt(index)) {    case 34: // "      escape = '&quot;';      break;    case 38: // &      escape = '&amp;';      break;    case 39: // '      escape = '&#x27;';      break;    case 60: // <      escape = '&lt;';      break;    case 62: // >      escape = '&gt;';      break;    default:      continue;    }  }
复制代码


这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下:


// 一段恶意代码<img src="empty.png" onerror ="alert('xss')"> // 转义后输出到 html 中&lt;img src=&quot;empty.png&quot; onerror =&quot;alert(&#x27;xss&#x27;)&quot;&gt; 
复制代码


这样就有效的防止了 XSS 攻击。


JSX 语法


JSX 实际上是一种语法糖,Babel 会把 JSX 编译成 React.createElement() 的函数调用,最终返回一个 ReactElement,以下为这几个步骤对应的代码:


// JSXconst element = (  <h1 className="greeting">    Hello, world!  </h1>);// 通过 babel 编译后的代码const element = React.createElement(  'h1',  {className: 'greeting'},  'Hello, world!');// React.createElement() 方法返回的 ReactElementconst element = {  $$typeof: Symbol('react.element'),  type: 'h1',  key: null,  props: {    children: 'Hello, world!',      className: 'greeting'     }  ...}
复制代码


我们可以看到,最终渲染的内容是在 Children 属性中,那了解了 JSX 的原理后,我们来试试能否通过构造特殊的 Children 进行 XSS 注入,来看下面一段代码:


const storedData = `{  "ref":null,  "type":"body",  "props":{  "dangerouslySetInnerHTML":{  "__html":"<img src=\"empty.png\" onerror =\"alert('xss')\"/>"      }  }}`;// 转成 JSONconst parsedData = JSON.parse(storedData);// 将数据渲染到页面render () {  return <span> {parsedData} </span>; }
复制代码


这段代码中, 运行后会报以下错误,提示不是有效的 ReactChild


Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {ref, type, props}). If you meant to render a collection of children, use an array instead.
复制代码


那究竟是哪里出问题了?我们看一下 ReactElement 的源码:


const symbolFor = Symbol.for;REACT_ELEMENT_TYPE = symbolFor('react.element');const ReactElement = function(type, key, ref, self, source, owner, props) {  const element = {    // 这个 tag 唯一标识了此为 ReactElement    $$typeof: REACT_ELEMENT_TYPE,    // 元素的内置属性    type: type,    key: key,    ref: ref,    props: props,    // 记录创建此元素的组件    _owner: owner,  };  ...  return element;}
复制代码


注意到其中有个属性是 $$typeof,它是用来标记此对象是一个 ReactElement,React 在进行渲染前会通过此属性进行校验,校验不通过将会抛出上面的错误。React 利用这个属性来防止通过构造特殊的 Children 来进行的 XSS 攻击,原因是 $$typeof 是个 Symbol 类型,进行 JSON 转换后会 Symbol 值会丢失,无法在前后端进行传输。如果用户提交了特殊的 Children,也无法进行渲染,利用此特性,可以防止存储型的 XSS 攻击。


在 React 中可引起漏洞的一些写法

使用 dangerouslySetInnerHTML


dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易使用户暴露在 XSS 攻击下,因为当使用 dangerouslySetInnerHTML 时,React 将不会对输入进行任何处理并直接渲染到 HTML 中,如果攻击者在 dangerouslySetInnerHTML 传入了恶意代码,那么浏览器将会运行恶意代码。看下源码:


function getNonChildrenInnerMarkup(props) {  const innerHTML = props.dangerouslySetInnerHTML; // 有dangerouslySetInnerHTML属性,会不经转义就渲染__html的内容  if (innerHTML != null) {    if (innerHTML.__html != null) {      return innerHTML.__html;    }  } else {    const content = props.children;    if (typeof content === 'string' || typeof content === 'number') {      return escapeTextForBrowser(content);    }  }  return null;}
复制代码


所以平时开发时最好避免使用 dangerouslySetInnerHTML,如果不得不使用的话,前端或服务端必须对输入进行相关验证,例如对特殊输入进行过滤、转义等处理。前端这边处理的话,推荐使用白名单过滤 (https://jsxss.com/zh/index.html),通过白名单控制允许的 HTML 标签及各标签的属性。


通过用户提供的对象来创建 React 组件


举个例子:

// 用户的输入const userProvidePropsString = `{"dangerouslySetInnerHTML":{"__html":"<img onerror='alert(\"xss\");' src='empty.png' />"}}"`;// 经过 JSON 转换const userProvideProps = JSON.parse(userProvidePropsString);// userProvideProps = {//   dangerouslySetInnerHTML: {//     "__html": `<img onerror='alert("xss");' src='empty.png' />`//      }// };render() {     // 出于某种原因解析用户提供的 JSON 并将对象作为 props 传递    return <div {...userProvideProps} /> }
复制代码


这段代码将用户提供的数据进行 JSON 转换后直接当做 div 的属性,当用户构造了类似例子中的特殊字符串时,页面就会被注入恶意代码,所以要注意平时在开发中不要直接使用用户的输入作为属性。


使用用户输入的值来渲染 a 标签的 href 属性,或类似 img 标签的 src 属性等


const userWebsite = "javascript:alert('xss');";<a href={userWebsite}></a>
复制代码


如果没有对该 URL 进行过滤以防止通过 javascript: 或 data: 来执行 JavaScript,则攻击者可以构造 XSS 攻击,此处会有潜在的安全问题。用户提供的 URL 需要在前端或者服务端在入库之前进行验证并过滤。


服务端如何防止 XSS 攻击


服务端作为最后一道防线,也需要做一些措施以防止 XSS 攻击,一般涉及以下几方面:


  • 在接收到用户输入时,需要对输入进行尽可能严格的过滤,过滤或移除特殊的 HTML 标签、JS 事件的关键字等。

  • 在输出时对数据进行转义,根据输出语境 (html/javascript/css/url),进行对应的转义

  • 对关键 Cookie 设置 http-only 属性,JS 脚本就不能访问到 http-only 的 Cookie 了

  • 利用 CSP (https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP) 来抵御或者削弱 XSS 攻击,一个 CSP 兼容的浏览器将会仅执行从白名单域获取到的脚本文件,忽略所有的其他脚本 (包括内联脚本和 HTML 的事件处理属性)


总结


出现 XSS 漏洞本质上是输入输出验证不充分,React 在设计上已经很安全了,但是一些反模式的写法还是会引起安全漏洞。Vue 也是类似,Vue 做的安全措施主要也是转义,HTML 的内容和动态绑定的属性都会进行转义。无论使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻击,所以服务端必须对前端参数做一些验证,包括但不限于特殊字符转义、标签、属性白名单过滤等。一旦出现安全问题一般都是挺严重的,不管是敏感数据被窃取或者用户资金被盗,损失往往无法挽回。我们平时开发中需要保持安全意识,保持代码的可靠性和安全性。



头图:Unsplash

作者:陈吉

原文:https://mp.weixin.qq.com/s/HweEFh78WXLawyQr_Vsl5g

原文:浅谈 React 中的 XSS 攻击

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


2021 年 5 月 03 日 06:062166

评论

发布
暂无评论
发现更多内容

Kubernetes介绍篇:是什么?为什么要用?

xcbeyond

Docker Kubernetes 容器 28天写作 Kubernetes从入门到精通

图解 | 原来这就是网络

云流

编程 网络 计算机

龙归科技 |企业办公自动化的未来

龙归科技

【小菜学网络】MAC地址详解

fasionchan

网络编程 网络协议 TCP/IP

全网独家首发!—份破解大厂面试官千层套路的算法+数据结构笔记!真是太TM重要了

比伯

Java 架构 面试 程序人生 算法

TRX智能合约系统开发案例详解

系统开发咨询:I76-883I-5I52 邓森

DevSecOps:好处和挑战

啸天

敏捷开发 运维自动化 DevSecOps 应用安全

三分钟快速掌握 maven插件

田维常

maven

HBase 底层原理详解(深度好文,建议收藏)

五分钟学大数据

大数据 HBase

京东搜索排序在线学习的 Flink 优化实践

Apache Flink

flink

区块链:行业应用即将“引爆”

CECBC区块链专委会

区块链

【TF2系列笔记】Day01:在VSCode中创建开发环境

IT蜗壳-Tango

七日更 TF2

智慧building之一 智能家居

张老蔫

28天写作

不要用+""代替强转

BerryMew

Docker真的被Kubernetes放弃了吗?

蔡超

Docker Kubernetes 云原生

springboot整合Shiro

Java架构师迁哥

案例研究之聊聊 QLExpress 源码 (五)

小诚信驿站

刘晓成 小诚信驿站 28天写作 QLExpress源码 聊聊源码

枪手博弈 - 在强者的世界,弱者的生存法则

石云升

博弈论 28天写作 枪手博弈

工信部:推动区块链等与工业互联网的融合技术研究

CECBC区块链专委会

大数据

醒醒!Python已经支持中文变量名啦!

Python猫

Python

一周信创舆情观察(1.4~1.10)

统小信uos

币值管理机器人系统开发|量化交易系统开发

W13902449729

币值管理机器人系统开发 量化交易系统开发

量化交易系统开发软件源码

系统开发咨询:I76-883I-5I52 邓森

【Mysql-InnoDB 系列】锁定读

程序员架构进阶

MySQL innodb 锁机制 28天写作

keycloak集群化的思考

程序那些事

架构设计 架构师 权限系统 程序那些事 集群服务

“直男”审美?不存在的!来看看 “攻城狮”对一款IoT App的UI改造吧!

IoT云工坊

android App 物联网 IoT sdk

玩一玩Linux常见命令第二篇

程序员的时光

程序员 28天写作

运维大规模ES集群的思考和实践

京东科技开发者

数据库 elasticsearch 数据分析

Spring Boot 中的MVC支持

武哥聊编程

Java mvc springboot SpringBoot 2 28天写作

区块链未来三年内将广泛落地

CECBC区块链专委会

区块链

RocketMQ中的事务消息

废材姑娘

RocketMQ

浅谈 React 中的 XSS 攻击-InfoQ