
SafeScript 发布:可安全执行不可信代码的 JavaScript 子集
来源标题:Show HN: SafeScript, a safe version of JavaScript for executing untrusted code
SafeScript 是一个严格受限的 JavaScript 子集,用于安全执行不可信代码,可编译为普通 JS 和 PHP。它禁用 eval、new、全局变量和原型访问,编译器压缩后约 48 KB(gzip 后约 12 KB)。该项目已在 Hacker News 的 Show HN 板块发布。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
用于代码的 JSON。JavaScript 的安全计算子集。
JSON 为世界提供了一种安全的数据交换方式。SafeScript 为计算做了同样的事。它是 JavaScript 的严格子集——没有 eval、没有 new、没有全局变量、没有原型访问——可编译为纯 JS 和 PHP。打开一个 .safe 文件,你看到的就是可以安全运行的代码。编译器压缩后约 48 KB(gzip 后约 12 KB)。可部署到任何地方,本地即可验证,无需信任。
SafeScript 像当年的 JSON 一样,从小处起步。任何你需要为数据或事件提供安全处理器的地方:
// Node.js const SafeScript = require('safescript'); const result = await SafeScript.run('return x + y;', { x: 10, y: 20 }); // → 30 // 安全全局对象默认可用——无需注入 await SafeScript.run('return Math.floor(3.7);'); // → 3 await SafeScript.run('return JSON.parse("{}");'); // → {}(null 原型,proto 已剥离) await SafeScript.run('return Object.keys({a:1});'); // → ['a'] // Math.random 默认是确定性的(mulberry32 PRNG,种子 0) await SafeScript.run('return Math.random();'); // → 每次返回相同的值
<!-- 浏览器 --> <script src="safescript.min.js"></script> <script> SafeScript.run('return n * 2;', { n: 21 }).then(function(result) { // → 42 }); </script>.safe 文件格式 SafeScript 文件使用 .safe 扩展名和 application/safescript MIME 类型。一个 .safe 文件是自包含的——它在顶部声明自己需要哪些能力,由宿主提供这些能力。
/**
- @safescript 0.2
- @needs request: Request
- @needs db: Database
- @uses logger: Logger
- @env browser, node / var body = request.json(); if (logger) logger.log('received', body); return db.store(body); 头部是一个可选的 /* */ 块注释,包含五个指令:
/**
- @safescript 0.2
- @needs fetch: function
- @needs log: function
- @type Response { json, text, status }
- @type Stream { get, set, fields } */ var resp = fetch('/api/data'); var data = resp.json(); log(data); 当 load() 处理此头部时,它会检查:
这样能在脚本运行之前、而不是等到第一次方法调用失败时,就及早发现不匹配。如果脚本声明了 @type Response { json, text, status },但宿主只允许 json 和 text,load() 会立即抛出错误。
一个 .safe 文件是一个信任单元——一套声明,一个沙箱边界。关于其安全状况的一切,都在单个文件中可见:
如果你想要两个具有不同访问模式的函数,就把它们放在两个 .safe 文件中。每个文件都有自己的作用域、自己的类型声明和自己的沙箱。文件本身就是安全边界。
这是有意为之。SafeScript 保护的是宿主环境——它确保用户提交的代码只能调用宿主明确允许的内容。用户保护(确保宿主按照用户的利益行事)来自完全不同的层面——关于宿主的 attestation,而不是 SafeScript 中的约束。关于这些层面如何组合,参见 Safebots layers。
一个 .safe 文件声明它需要什么。它不 import、不 require、也不 fetch 任何东西。宿主读取声明并构建作用域——宿主从哪里获得实现是它自己的事。Node 宿主可能从 node_modules 解析。浏览器宿主可能使用 import map 或 CDN。沙箱环境可能从精选的白名单中获取。.safe 文件既不知道也不关心。
这是对 require() 和 import 的有意背离。包管理器是特定宿主的实现细节,而不是语言中的顶层概念。一个依赖 npm 才能运行的 .safe 文件并不是真正可移植的。一个声明 @needs fetch: http.Fetch 的 .safe 文件,在任何能提供 HTTP 客户端的宿主上都能工作。
这也避免了继承包管理器的供应链攻击面。SafeScript 的安全叙事始于文件边界——你阅读头部,你看到代码能接触什么,你决定是否运行它。
const SafeScript = require('safescript'); // 1. 解析头部 const caps = SafeScript.capabilities(source); // → { version: '0.2', env: ['browser', 'node'], // needs: [{name: 'request', type: 'Request'}, ...], // uses: [{name: 'logger', type: 'Logger'}] } // 2. 检查你能否满足这些要求 const check = SafeScript.checkScope(source, myScope); if (!check.ok) throw new Error('Missing: ' + check.missing.join(', ')); // 3. 用你构建的作用域运行 const result = await SafeScript.run(source, myScope); 工作原理 四道安全屏障,纵深防御:
Token/语法白名单——解析器只接受 JavaScript 的一个子集。没有 eval、new、delete、class、import、async、await、模板字面量,也不能访问 globalThis/window/document。AST 从头重建——原始源文本绝不会出现在输出中。
运行时属性守卫——方括号访问(obj[key])被重写为 __at(obj, key) / __set(obj, key, val),在运行时拦截原型链属性(constructor、proto、prototype、caller、callee),即使键是动态计算的。
全局遮蔽 + 安全全局对象——每个原生全局变量都在编译后的函数内部声明为 undefined,然后自动注入一组精选的安全全局对象:Math、JSON、Object、Array、String、Number、错误构造器以及 parseInt 和 encodeURI 等独立函数的冻结门面对象。这些门面只暴露安全的静态方法——绝不暴露原始构造器或可变原型。额外的能力通过 scope 对象注入。代码始终在 "use strict" 下运行。
执行守卫 + 钩子——编译期注入的 __check() 调用强制执行超时和操作限制(硬编码上限:60 秒,1 亿次操作),防止无限循环和无限递归。钩子拦截对注入的 scope 函数的每一次调用,让宿主能在边界处对资源进行限流。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。