HN 日本語サマリー

← 一覧へ戻る
プログラミング

Show HN: SafeScript, untrusted codeを実行するためのJavaScriptの安全なバージョン

Show HN: SafeScript, a safe version of JavaScript for executing untrusted code (github.com)

7 pointsby EGreg1 コメント

要約

SafeScriptは、JavaScriptの安全な計算サブセットであり、信頼できないコードを実行するためのソリューションです。`eval`や`new`などの危険な機能がなく、コンパイルするとプレーンなJSやPHPになります。これにより、Webサーバーハンドラ、イベントハンドラ、プラグインスクリプトなどで安全にコードを実行できます。

全文翻訳

SafeScript

コードのためのJSON。 JavaScriptの安全な計算サブセット。

JSONは、データ交換のための安全な方法を世界にもたらしました。SafeScriptも計算に関して同じことを行います。これはJavaScriptの厳密なサブセットであり、eval、new、グローバル変数、プロトタイプアクセスはありません。プレーンなJSおよびPHPにコンパイルされます。.safeファイルを開くと、実行しても安全なコードが表示されます。コンパイラは(gzip圧縮で約12KBの)約48KBです。どこにでも配布でき、ローカルで検証でき、信頼は不要です。

ユースケース

SafeScriptは、JSONが始まったのと同じように、小さく始まります。データのハンドラやイベントを安全に処理する必要がある場所ならどこでも使用できます。

  • Webサーバーハンドラ — ユーザーまたはテナントから送信されたルーティングロジック、リクエスト変換、検証ルール
  • onclickおよびイベントハンドラ — サンドボックスから脱出できないユーザー作成のブラウザイベントハンドラ
  • プラグインスクリプト — ホストを危険にさらすことなく、ユーザー提出のロジックでアプリを拡張する
  • 数式と式 — スプレッドシートスタイルの計算フィールド、価格設定ルール、スコアリング関数
  • ボットおよびエージェントツール — 構築上安全なAIエージェント用の呼び出し可能ツール(Safebotsを参照)
  • ロジックを含む設定 — JSONでは不十分だが、任意のJSは多すぎる場合

クイックスタート

// 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-prototype, __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);

ヘッダーはオプションの/** */ブロックコメントで、5つのディレクティブがあります。

  • @safescript <version> — 言語バージョン
  • @needs <name>: <Type> — 必要な機能。ホストはそれを提供するか、ファイルの実行を拒否する必要があります。
  • @uses <name>: <Type> — オプションの機能。ホストは利用可能であれば提供し、スクリプトはif (name)でチェックします。
  • @type <TypeName> { method1, method2 } — 返り値の型とスクリプトが呼び出そうとしているメソッドを宣言します。
  • @env <env1>, <env2> — このファイルを実行できる環境を制限します(オプション、カンマ区切り)。
型宣言

@type宣言は、スクリプトが呼び出そうとしている返り値の型上のメソッドをホストに伝えます。これらは純粋に宣言的です — ランタイムの強制を変更しませんが、load()時に事前チェックを可能にします。

/**
 * @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()がこのヘッダーを処理するとき、次のことをチェックします。

  1. すべての@type名がホストに登録されていること(SafeScript.lib()経由)。
  2. 宣言されたすべてのメソッドがホストのallowMethodsホワイトリストに含まれていること。

これにより、スクリプトが実行される前に不一致が早期に検出されます。スクリプトが@type Response { json, text, status }を宣言し、ホストがjsonとtextのみを許可する場合、load()はすぐにエラーをスローします。

.safe ファイルとしての信頼単位

.safeファイルは信頼の単位です — 1つの宣言セット、1つのサンドボックス境界。そのセキュリティ体制に関するすべてが1つのファイルで可視化されます。

  • 必要なもの (@needs) — インジェクトされた機能。
  • オプションで使用するもの (@uses) — 低減可能な機能。
  • 触れる返り値の型 (@type) — ホスト提供オブジェクト上のメソッド。
  • 期待する環境 (@env) — ランタイム制約。

異なるアクセスパターンを持つ2つの関数が必要な場合は、それらを2つの.safeファイルに配置します。各ファイルには独自のスコープ、型宣言、サンドボックスがあります。ファイル自体がセキュリティ境界です。

これは意図的な設計です。SafeScriptはホストの環境を保護します — ユーザー提出コードがホストが明示的に許可したものしか呼び出せないことを保証します。ユーザー保護(ホストがユーザーの利益のために行動することを保証する)は、まったく別のレイヤーから来ます — ホストに関するアテステーションであり、SafeScript内の制約ではありません。これらのレイヤーがどのように構成されるかについては、Safebotsレイヤーを参照してください。

設計による自己完結性

.safeファイルは必要なものを宣言します。インポート、require、またはfetchは行いません。ホストは宣言を読み取り、スコープを構築します — ホストが実装を取得する場所はホストの責任です。Nodeホストはnode_modulesから解決するかもしれません。ブラウザホストはインポートマップまたは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);

仕組み

4つのセキュリティバリア、多層防御:

  1. トークン/文法許可リスト — パーサーはJavaScriptのサブセットのみを受け入れます。eval、new、delete、class、import、async、await、テンプレートリテラル、globalThis/window/documentへのアクセスはありません。ASTはゼロから再構築されます — 元のソーステキストは出力に表示されません。

  2. ランタイムプロパティガード — ブラケットアクセス(obj[key])は__at(obj, key) / __set(obj, key, val)に書き換えられ、キーが動的に計算される場合でも、ランタイムでプロトタイプチェーンのプロパティ(constructor、__proto__、prototype、caller、callee)をブロックします。

  3. グローバルシャドーイング+安全なグローバル — すべてのネイティブグローバルは、コンパイルされた関数内でundefinedとして宣言され、キュレーションされた安全なグローバルセットが自動的にインジェクトされます:Math、JSON、Object、Array、String、Numberのフローズンファサードオブジェクト、エラーコンストラクタ、およびparseIntやencodeURIのようなスタンドアロン関数。これらのファサードは、安全な静的メソッドのみを公開します — 生のコンストラクタや変更可能なプロトタイプは公開しません。追加の機能はスコープオブジェクトを介してインジェクトされます。コードは常に"use strict"で実行されます。

  4. 実行ガード+フック — コンパイル時にインジェクトされた__check()呼び出しは、タイムアウトと操作制限(ハードコードされた上限:60秒、1億操作)を強制し、無限ループと無限再帰を防ぎます。フックはインジェクトされたスコープ関数へのすべての呼び出しをインターセプトし、ホストが境界でリソースをスロットリングできるようにします。

空のスコープで、SafeScriptは安全なグローバル変数を持つ純粋な計算です — 決定論的な数学、JSON処理、型チェック — ただし、I/O、副作用、関数外へのアクセスはありません。追加の機能は、オブジェクトキャパビリティモデルを使用してスコープオブジェクトを介してインジェクトされます。

実行モデル

.safeファイルは関数本体です。コンパイラはそれを関数でラップし、ホストはその関数をスコープで呼び出し、コードはreturnを使用して値を返します。run()は常にPromiseを返します。

// 計算された値を返す
await SafeScript.run('return price * qty;', { price: 9.99, qty: 3 });  // → 29.97

// 関数を返す(関数自体は非同期です)
var double = await SafeScript.run('return function(n) { return n * 2; };', {});
await double(21);  // → 42

// オブジェクトを返す — "モジュール"パターン
var mod = await SafeScript.run(`
  var count = 0;
  return {
    inc: function() { count = count + 1; return count; },
    get: function() { return count; }
  };
`, {});
await mod.inc(); await mod.inc(); await mod.get(); // → 2

// 副作用のみ(インジェクトされた機能に書き込み、戻り値なし)
await SafeScript.run('db.store(record);', { db: myDb, record: data });

exportもmodule.exportsもなく、暗黙の最後の式戻り値もありません。returnは意図を明確にし、.safeファイルを読みやすく保ちます。

自動非同期

SafeScriptは自動非同期実行モデルを使用します。コンパイラは自動的に次の処理を行います。

  • すべての関数(宣言、式、アロー、メソッドショートハンド)をasyncとして出力します。
  • すべての関数呼び出しをawaitでラップします。

これは、asyncとawaitがSafeScriptのキーワードではなく、ブロックされていることを意味します。コンパイラはそれらを透過的に処理します。これには2つの重要な結果があります。

  1. Promiseの自動解決。インジェクトされたオブジェクトがPromiseを返す場合、すべての呼び出しサイトで待機されます。未解決のPromiseは漏れません。
  2. 逐次実行の保証。単一のSafeScript実行内で同時に進行中の操作はありません。スループットは、1つ内の並行性からではなく、多数のSafeScriptインスタンスを並列実行すること(PHP-FPMワーカーのように)から得られます。
  3. 透過的なクロスコンテキストプロキシ。すべての外部呼び出しが待機されるため、SafeScriptコードはスコープオブジェクトがpostMessageを介してメインスレッドへの呼び出しをプロキシしている間に、WebWorkerで実行できます。SafeScriptソースは変更されません — それはすでに同期的に見えるコードとして書かれており、コンパイラはすでにすべての呼び出しを待機しています。これにより、Q.Safeboxのようなホストは、.safeファイルのコード変更なしで、メインスレッドから信頼できないスクリプト実行を移動できます。
// 同じSafeScriptソースは、スコープがローカルかプロキシされているかに関わらず同じように機能します:
var el = createElement("div")    // <- 自動待機、createElementが
setText(el, "Hello")             //    値を返すかPromiseを返すかに関わらず機能します
appendTo(el, container)          //    (例:MessageChannelプロキシから)
return getText(el)
実行ガード

SafeScriptは、コンパイル時のガードインジェクションにより、無限ループ、無限再帰、暴走する計算から保護します。

コンパイラは、すべての関数本体の先頭とすべてのループ反復の開始(すべての5つのループタイプ:for、for-in、for-of、while、do-while)に__check()呼び出しをインジェクトします。ランタイムでは、__check()は操作カウントと壁時計のデッドラインが超過していないことを検証します。いずれかの制限に達した場合、実行はすぐにスローされます — Promiseのタイムアウトを待つ必要はありません。

// 5秒後に中止
var result = await SafeScript.run(source, scope, { timeout: 5000 });

// 合計操作数を制限(ループ反復 + 関数エントリ)
var result = await SafeScript.run(source, scope, { maxOps: 10000 });

// 両方
var result = await SafeScript.run(source, scope, { timeout: 5000, maxOps: 50000 });

ハードコードされた上限。ホストがtimeoutを省略したり0を渡したりしても、60秒(MAX_TIMEOUT = 60000)の上限が常に強制されます。同様に、maxOpsは1億(MAX_OPS = 100000000)に制限されます。これらの上限は引き上げられません — それらは誤設定されたホストから保護します。

Promise.raceは、遅い非同期インジェクトメソッドの二次防御として残りますが、主な保護はコンパイル済みコード自体の__check()ガードです。

フック

フックにより、ホストはインジェクトされたスコープ関数へのすべての呼び出しをインターセプトできます。これがリソース使用量(データサイズ制限、呼び出し回数、レート制限、コストキャップ)を、SafeScriptが外部リソースと出会う境界でスロットリングする方法です。

var result = await SafeScript.run(source, scope, {
  hooks: {
    beforeCall: function(info) {
      // info = { name, args, callCount, elapsed }
      // name はドット表記: "db.query", "api.users.list"

      // 特定の関数をブロック
      if (info.name === 'db.drop') return false;

      // 呼び出し回数制限を強制
      if (info.callCount > 100) throw new Error('too many calls');

      // キャッシュされた/デフォルト値でショートサーキット
      if (info.name === 'fetch' && rateLimited) return { value: null };

      // 引数を変換
      if (info.name === 'db.query') return { args: [sanitizeSQL(info.args[0])] };

      // 通常の処理を続行するためにundefinedを返す
    },
    afterCall: function(info) {
      // info = { name, args, result, callCount, elapsed }

      // レスポンスサイズを確認
      if (JSON.stringify(info.result).length > 1000000) {
        throw new Error('response too large');
      }

      // 結果を変換
      return { value: redact(info.result) };
    }
  }
});

beforeCall(info) — 各スコープ関数呼び出しの前に呼び出されます。返り値:

  • undefined — 通常の処理を続行
  • false — 呼び出しを抑制(スクリプトにはundefinedを返します)
  • { value: x } — ショートサーキット、xを返します(nullも含む、正常な低下のため)
  • { args: [...] } — 引数を変換してから呼び出します。
  • throw — 実行を終了します。

afterCall(info) — 各スコープ関数が返された後に呼び出されます。同じ返り値プロトコルに加え、info.resultにアクセスできます。

フックはスコープオブジェクトを介して関数を再帰的にラップします。{ db: { query: fn, insert: fn } }をインジェクトする場合、フックは名前として"db.query"と"db.insert"を見ます。

ホストレベルの合成

SafeScriptファイルは互いにインポートしません。合成はホストレベルで行われます — 1つのSafeScriptを実行してユーティリティを生成し、その結果を別のものにインジェクトします。

// ユーティリティライブラリをビルド
var utils = await SafeScript.run(utilsSource, { Math });

// ハンドラにインジェクト
var result = await SafeScript.run(handlerSource, { utils, db, request });

これにより、各.safeファイルは自己完結性を保ちつつ、ホストが自由にそれらを合成できます。ホストが依存関係グラフを制御し、スクリプトは制御しません。

安全なグローバル変数

SafeScriptは、すべての実行で利用可能なフローズンファサードオブジェクトのセットを提供します — インジェクション不要です。これらはJavaScriptの対応物の安全なサブセットです:静的メソッドのみ、コンストラクタなし、変更不可能なプロトタイプなし。

グローバル利用可能なもの
Objectkeys, values, entries, assign, freeze, isFrozen, getOwnPropertyNames, create (nullのみ)
ArrayisArray, from, of
StringfromCharCode, fromCodePoint
NumberisInteger, isFinite, isNaN, parseInt, parseFloat, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, EPSILON, MAX_VALUE, MIN_VALUE, POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN
Mathすべての標準メソッド。Math.randomはデフォルトで決定論的です(下記参照)。
JSONparse(サニタイズ済み — 出力はnull-prototypeオブジェクトを使用し、__proto__キーは削除されます)、stringify
Error, TypeError, RangeError, SyntaxError, ReferenceErrorコンストラクタ(throwで使用可能)
スタンドアロンparseInt, parseFloat, isNaN, isFinite, encodeURI, decodeURI, encodeURIComponent, decodeURIComponent
定数NaN, Infinity

すべてのファサードオブジェクトは深くフリーズされています。それらを変更しようとする試み(例:Math.PI = 3)は、厳格モードではサイレントに失敗します。Object.createはObject.create(null)に制限されています — それ以外の引数を渡すとエラーになります。

JSON.parseのサニタイズ

JSON.parseの出力はディープウォークされ、すべてのオブジェクトをnull-prototypeオブジェクトに変換し、__proto__キーを削除します。これにより、解析されたJSONデータを通じたプロトタイプ汚染攻撃を防ぎます。

決定論的RNG

Math.randomは、呼び出しごとにシードされたマルベリー32 PRNGを使用します。デフォルトではシードは0なので、run()のすべての呼び出しで同じ乱数列が生成されます。これにより、SafeScriptの実行は完全に再現可能になります。

// 決定論的 — 毎回同じ結果
await SafeScript.run('return Math.random();');  // → 常に0.2267...

// カスタムシード
await SafeScript.run('return Math.random();', {}, { seed: 42 });  // → 常に0.0020...

// 真のランダム性(ホストの承認が必要)
await SafeScript.run('return Math.random();', {}, {
  seed: null,
  env: { Random: true }
});

seedオプションは動作を制御します。

seed値動作
省略 / undefined決定論的、シード = 0
任意の整数決定論的、そのシード
null真のMath.random — env: { Random: true }が必要

env: { Random: true }なしでseed: nullを設定すると、SafeScript: seed: null requires env: { Random: true } (nondeterministic RNG needs host approval)というエラーが発生します。これにより、非決定性は常にホストの明示的な決定となります。

ライブラリ登録

ホストは、load()が@needsおよび@uses宣言のために自動的に解決する名前付きライブラリを登録できます。これにより、一般的な機能の解決ロジックの繰り返しを回避できます。

// ライブラリを一度だけ起動時に登録
SafeScript.lib('http', myHttpClient);
SafeScript.lib('db', myDbAdapter);

// @needs http または @needs db を持つスクリプトは自動的にそれらを取得します
var bundle = await SafeScript.load(source);  // リゾルバ不要
await SafeScript.run(bundle);

// リゾルバスコープは登録済みライブラリを優先します
var bundle = await SafeScript.load(source, function(caps) {
  return { http: customHttpClient };  // 登録済みの'http'を上書き
});

// 登録の管理
SafeScript.libs();       // → ['http', 'db']
SafeScript.unlib('db');  // 登録解除

登録されていない@needsは、リゾルバがそれらを提供しない場合、依然としてMissing capabilitiesエラーをスローします。

型レジストリ

ホスト関数がオブジェクトを返す場合、それらのオブジェクトはメソッドを運びます。登録がない場合、SafeScriptは未知の型に対するすべてのメソッド呼び出しをブロックします — サンドボックス違反です。型レジストリにより、ホストは安全に呼び出せるメソッドを正確に宣言できます。

// 型とその許可メソッドを登録
SafeScript.lib(Counter, { allowMethods: ['value', 'increment', 'toString'] });

// これでSafeScriptコードはそれらのメソッドを呼び出せるようになります — そしてそれらのみ
var result = await SafeScript.run('return c.value()', { c: new Counter(42) });
// → 42

// ブロックされたメソッドはエラーをスローします
await SafeScript.run('c.reset()', { c: new Counter(0) });
// → Error: SafeScript: method "reset" not allowed on Counter

// 未登録の型はすべてをブロックします
await SafeScript.run('return x.anything()', { x: new SomeClass() });
// → Error: SafeScript: cannot call method on unregistered type
名前付き型登録(@typeヘッダー用)

3引数形式は、文字列名(@typeヘッダー検証用)とコンストラクタ(ランタイム強制用)の両方を登録します。

// 3引数: lib(name, Constructor, config)
SafeScript.lib('Response', ResponseClass, { allowMethods: ['json', 'text', 'status'] });
SafeScript.lib('Stream', StreamClass, { allowMethods: ['get', 'set', 'fields'] });

// これで、.safeヘッダーの@type Response { json, text } はload()時に検証されます
var bundle = await SafeScript.load(source);  // @type宣言をチェックします
await SafeScript.run(bundle);
仕組み

ランタイムは、すべてのメソッド呼び出しをディスパッチチェーンを通じてチェックします。

  1. プリミティブ(文字列、数値、ブール値)→ すべてのネイティブメソッドを許可します。
  2. 配列 → すべてのネイティブメソッドを許可します。
  3. 関数 → すべてを許可します(call、apply、bind)。
  4. プレーンオブジェクト → 呼び出し可能なプロパティのみを許可します。
  5. エラーファミリー → すべてを許可します(toString、messageなど)。
  6. 登録済み型 → allowMethodsホワイトリストをチェックします。
  7. 未登録型 → エラーをスローします(サンドボックス違反)。

サブクラス継承は自動的に機能します — SpecialCounter extends Counterインスタンスは、SpecialCounterが個別に登録されていない場合、Counter登録を使用します。

// 登録解除
SafeScript.unlib(Counter);

// すべての型登録をクリア
// (ランタイム内部からアクセス可能)

thisとファサード

SafeScriptはthisをキーワードとしてサポートします。これは、変更可能なセルパターンを介して内部の__safeThis識別子にコンパイルされます。デフォルトでは、thisはどこでもundefinedです — トップレベル、通常の関数、アロー関数。

ホストは、facadesオプションを介してthisが解決されるものを制御します。ファサードは、インジェクトされたコールバックを受け入れる関数が、受信するコールバックのthisを設定できるようにします。

var result = await SafeScript.run(`
  Q.Tool.define("my-tool", {
    init: function() {
      // 'this' は createToolFacade によって作成されたファサードオブジェクトです
      this.state.count = 0;
    },
    methods: {
      increment: function() {
        this.state.count = this.state.count + 1;
      }
    }
  });
`, { Q: myQObject }, {
  facades: {
    'Q.Tool.define': {
      args: [1],        // コールバックの引数インデックス
      methods: [3],     // メソッドオブジェクトの引数インデックス(値はラップされます)
      create: function(name) { return { state: {}, element: null }; }
    }
  }
});

ファサード設定フィールド:

  • args — コールバックである引数のインデックス(thisがファサードに設定されます)。
  • methods — コールバックであるオブジェクトの引数のインデックス(値はラップされます)。
  • create — ファサードオブジェクトを構築するために、他の引数とともに呼び出されるファクトリ関数。

this上のすべてのプロパティアクセスは、他のオブジェクトと同様に__at/__setガードを通過します — constructor、__proto__、prototypeはブロックされます。

API

SafeScript.run(source, scope?, opts?) または SafeScript.run(bundle)

コンパイルして実行します。トップレベルのreturnステートメントからの値に解決されるPromise、またはそれがない場合はundefinedを返します。

await SafeScript.run('return a + b;', { a: 1, b: 2 }); // → 3

// または load() からのバンドルを渡す
var bundle = await SafeScript.load(source, resolver);
await SafeScript.run(bundle); // 同じ結果

オプション:

  • timeout: N — ミリ秒単位の実行タイムアウト(60000に制限)。省略されても常に強制されます。
  • maxOps: N — 最大操作数/ガードチェック(100000000に制限)。省略されても常に強制されます。
  • hooks: { beforeCall, afterCall } — インジェクトされたスコープ関数への呼び出しをインターセプトします。Hooksを参照してください。
  • seed: N — PRNGシード。デフォルトは0(決定論的)。カスタムシードの場合は任意の整数。nullは真のMath.random(env: { Random: true }が必要)です。
  • env: { Random: true } — 環境機能を承認します。現在定義されているのはRandomのみです。
  • facades: { ... } — メソッドごとのthisファサード。 thisとファサードを参照してください。
SafeScript.compile(source)

{ code, ast, hash }を返します。codeは安全なJavaScriptです。hashは正規化されたASTのSHA-256です(ブラウザではnull — hashASTAsyncを使用してください)。

SafeScript.validate(source)

{ ok: boolean, errors: Array|null }を返します。実行せずにチェックします。

SafeScript.capabilities(source)

{ version, env, needs: [{name, type}], uses: [{name, type}], types: [{name, methods}] }を返します。.safeファイルのヘッダーを解析します。

SafeScript.load(source, resolver?)

非同期ワンコール統合。ヘッダーを解析し、resolver(caps)を呼び出してスコープを構築します(リゾルバは同期または非同期にできます)。@needsを検証し、{ source, scope }バンドルを解決するPromiseを返します。これをrun()に渡します。

// ロードしてから実行
var bundle = await SafeScript.load(source, function(caps) {
  return {
    request: reqWrapper,
    db: dbHandle,
    logger: caps.uses.find(u => u.name === 'logger') ? myLogger : undefined
  };
});
var result = await SafeScript.run(bundle);

// 一度準備して、複数回実行
var bundle = await SafeScript.load(templateSource, resolver);
for (var req of requests) {
  bundle.scope.request = req;
  results.push(await SafeScript.run(bundle));
}

リゾルバが提供されない場合、空のスコープで解決します(@needsのないスクリプトには問題ありません)。スコープが@needsを満たさない場合は拒否されます。

SafeScript.checkScope(source, scope)

{ ok: boolean, missing: string[] }を返します。スコープオブジェクトがファイルの@needs宣言を満たしているか確認します。

SafeScript.parse(source)

AST(ESTreeライク)を返します。

SafeScript.sanitize(obj)

オブジェクトをディープコピーしてnull-prototypeオブジェクトにし、プロトタイプチェーンのエスケープをブロックします。

SafeScript.lib(name, value) / SafeScript.lib(Constructor, config) / SafeScript.lib(name, Constructor, config)

3つの形式:

  • lib(name, value) — 名前付きライブラリを登録します(load()によって@needs/@uses用に自動解決されます)。
  • lib(Constructor, { allowMethods: [...] }) — ランタイムメソッド強制のための型ホワイトリストを登録します。
  • lib(name, Constructor, { allowMethods: [...] }) — 名前付き型(@typeヘッダー検証用)とコンストラクタホワイトリスト(ランタイム強制用)の両方を登録します。
SafeScript.unlib(name) / SafeScript.unlib(Constructor)

名前付きライブラリ、型登録、または名前付き型マッピングを登録解除します。

SafeScript.libs()

登録済みライブラリ名の配列を返します。

SafeScript.clearTypeRegistry()

すべての型登録(コンストラクタホワイトリストと名前付き型マッピングの両方)をクリアします。

許可されているもの

SafeScriptは、ES2020の広範な計算サブセットをサポートしています。

  • 変数:var、let、const
  • 制御フロー:if/else、for、for-in、for-of、while、do-while、switch/case、break、continue、return、ラベル付きステートメント
  • 関数:宣言、式、アロー、デフォルトパラメータ、レストパラメータ、this(ファサード経由)
  • 式:すべての算術、比較、論理、三項、typeof、void、instanceof、nullish合体(??)、オプショナルチェイニング(?.)
  • 分割代入:配列とオブジェクトパターン、レスト要素、ネスト
  • スプレッド:配列とオブジェクト
  • エラー処理:try/catch/finally、throw
  • リテラル:文字列、数値(10進数、16進数、2進数、8進数、BigInt)、正規表現、配列、オブジェクト

ブロックされているもの

  • 識別子:eval、Function、globalThis、window、document、require、process、Proxy、Reflect、Symbol、fetch、setTimeoutなど。
  • キーワード:new、delete、class、import、export、with、debugger、yield、async、await。
  • プロパティ(ブラケットアクセス):constructor、__proto__、prototype、caller、callee。
  • 構文:テンプレートリテラル、ジェネレータ(function*)。

仕様

正式な言語定義については、Specification.mdを参照してください — ES8から始まり、削除されたもの、書き換えられたもの、追加されたものをリストアップしています。

Examples.mdを参照してください。基本的なもの、制御フロー、関数、分割代入、スプレッド、および実際のパターン(リクエストハンドラ、価格設定エンジン、バリデーター、ステートマシン)の包括的なコレクションです。

サイズ

ビルドサイズ
safescript.js約94KB
safescript.min.js約48KB
gzip圧縮約12KB

エコシステム

一般的な環境向けの統合パッケージ:

パッケージ機能
safescript-expressExpressミドルウェア — .safeファイルをルートにマッピングし、機能解決を行う
safescript-loaderブラウザ自動ローダー(<script type="safescript">要素用)
safescript-cliCLIツール — .safeファイルの実行、検証、チェック、コンパイル、検査
safescript-quickjsQuickJSを介してC/C++/RustアプリにSafeScriptを埋め込む
safescript-php純粋なPHPポート — Node依存なしでPHPでSafeScriptを実行

テスト

node test.js               # v0.1 リンター/ラッパー
node test-compiler.js      # コンパイラパイプライン
node test-battle.js        # 敵対的/エスケープ試行
node test-header.js        # ヘッダーパーサー + load() + run(bundle)
node test-guards-hooks.js  # 実行ガード + フックシステム
node test-type-registry.js # 型レジストリ — メソッドホワイトリスト
node test-type-header.js   # @type ヘッダー宣言 + load() 検証

ライセンス

MIT — Gregory Magarshak