前端如何安全执行字符串js

9 篇文章 0 订阅

前言

之前有功能需求,需要在前端页面上执行用户自定义的字符串js。直接的操作可以用eval或者new Function来执行字符串脚本。但是这样很不安全,获取cookie、获取隐私、发送请求等等代码块都有可能被恶意者故意注入进去。其实最好的方案,就是让后端去执行这段自定义脚本,返回结果给前端。当然,本次主要想解决的是除了这个方案,还有什么办法?网上找一番,可以较为安全地执行自定义脚本的方法有

  • js解释器
  • with + proxy
  • 利用iframe的沙盒模式

利用js解释器,就是采用第三方包解释器来执行自定义脚本,如

const code = '1 + 2'
const interpreter = new Interpreter(code)
interpreter.run()
console.log(interpreter.value) // 3

with + proxy就是在withscope里执行自定义脚本,并且对with语句指定对象进行代理,这样一来,使得with语句里的代码块不会向上访问到外部的变量,达到避免自定义脚本恶意获取隐私的目的。具体参考

const sandbox = { a: 123 }
const sandboxProxy = new Proxy(sandbox, { has })
function has (target, key) {
  return true
}
with(sandboxProxy) {
  // code... 
}

最后推荐使用iframe的沙盒模式来执行自定义脚本,这可以真正达到约束脚本执行。大概实现思路就是,在需要执行自定义脚本的页面加入iframe,并且设置sandbox属性为allow-scripts,利用message通信,将需要执行的代码发给frame页面,在该页面执行代码,并把结果返回给主页面。

利用iframe的沙盒模式

主要的代码功能就是在主页面发送代码字符串给frame页面

const frame = window.document.createElement('iframe')
frame.src = 'www.test.com/frame'
frame.sandbox = 'allow-scripts'
frame.style.display = 'none'
window.document.body.appendChild(frame)

// 发送自定义代码
const str = 'return 1 + 2'
frame.postMessage(str, '*')

等待接受执行结果

const reciveMessage = e => {
  // 验证来源
  if (e.origin === 'null' && e.source === frame.contentWindow) {
    // 收到iframe的代码执行结果
    console.log(e.data)
  }
}
window.addEventListener('message', reciveMessage, false)

frame页面执行代码,自定义代码若想获取cookielocalStorage等,都会报错

function reciveMessage(e) {
  // 相当于window.top.currentWindow
  const mainWindow = e.source

  let result
  try {
    result = Function(e.data)()
  } catch (err) {
    result = '失败'
  }
  // 将结果返回给主页面
  mainWindow.postMessage(result, e.origin)
}
window.addEventListener('message', reciveMessage, false)

具体代码

由于最近做的是react项目,所以是用基于hook写的示例

frame页面

// frame页面
import { useEffect } from 'react'

function reciveMessage(e) {
  if (window.location.origin !== e.origin) {
    return
  }
  
  // 相当于window.top.currentWindow
  const mainWindow = e.source

  let result
  try {
    result = Function(e.data)()
  } catch (err) {
    console.warn('失败', err)
    result = '失败'
  }

  mainWindow.postMessage(result, e.origin)
}

export default function Frame() {
  useEffect(() => {
    window.addEventListener('message', reciveMessage, false)

    return () => {
      window.removeEventListener('message', reciveMessage, false)
    }
  }, [])

  return null
}

主页面

// index页面
import { useEffect, useRef } from 'react'

function useScript(str, callback) {
  const frameRef = useRef()

  useEffect(() => {
    if (str) {
      if (!frameRef.current) {
        // 建立iframe的沙盒模式
        const frame = window.document.createElement('iframe')
        frame.src = `${window.location.origin}/frame`
        frame.sandbox = 'allow-scripts'
        frame.style.display = 'none'
        frameRef.current = frame
        window.document.body.appendChild(frame)
      }

      const reciveMessage = e => {
        // 进行信息来源的验证
        if (e.origin === 'null' && e.source === frameRef.current.contentWindow) {
          // 收到iframe的代码执行结果
          callback(e.data)
        }
      }

      window.addEventListener('message', reciveMessage, false)
      return () => {
        window.removeEventListener('message', reciveMessage, false)
      }
    }
  }, [str, callback])

  function onValidateScript(data) {
    // 提供自定义代码需要的数据
    const dataCode = `
      "use strict";
      var data = ${JSON.stringify(data)};
    `
    const iframe = frameRef.current
    const iframeWin = iframe.contentWindow
    // 向iframe发送字符串代码
    iframeWin.postMessage(dataCode + str, '*')
  }

  return onValidateScript
}

export default function Index() {
  const customCode = 'return data + 2'
  
  const onValidateScript = useScript(
    customCode,
    data => console.log(data) // 3
  )
  
  return (
  	<button onClick={() => onValidateScript(1)}>执行脚本</button>
  )
}

参考资料

https://www.imooc.com/article/17353

https://zhuanlan.zhihu.com/p/46571509

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值