はじめに
プリザンターのサーバスクリプトは現在 JavaScript のみをサポートしていますが、「Python でも書けたらいいのに」と思ったことはありませんか?
今回はプリザンターのサーバスクリプトの基盤技術である ClearScript の仕組みを理解した上で、.NET 上で動作する Python 実装 IronPython を使って Python 対応が実現可能かどうかを調べてみます。
ボリュームがあるため2部構成です。
| 記事 | 内容 |
|---|---|
| 前編(この記事) | ClearScript とは何か、なぜ採用されているのか、IronPython で Python 対応できるのか |
| 後編 | IronPython のサンドボックス実装の具体的な設計 |
バージョン 1.5.1.0 を対象にしています
ClearScript ってどんなもの?
まずは現行のサーバスクリプトを支えている ClearScript について整理しておきます。
概要
ClearScript は Microsoft が開発している .NET 向けのスクリプトエンジンホスティングライブラリです。Google の V8 エンジン(Chrome や Node.js で使われている JavaScript エンジン)を .NET アプリケーションに組み込むことができます。
プリザンターでは NuGet パッケージ Microsoft.ClearScript.Complete を参照していて、サーバスクリプトの実行は全てこの V8 エンジン上で行われています。
<PackageReference Include="Microsoft.ClearScript.Complete" Version="7.5.0" />
プリザンターでの使われ方
プリザンターの ScriptEngine クラスは ClearScript の V8ScriptEngine を薄くラップしたものです。実際のコードを見てみましょう。
public ScriptEngine(bool debug)
{
var flags = V8ScriptEngineFlags.EnableDateTimeConversion;
if (debug)
{
flags |= V8ScriptEngineFlags.EnableDebugging
| V8ScriptEngineFlags.EnableRemoteDebugging;
}
v8ScriptEngine = new V8ScriptEngine(flags);
}
そしてサーバスクリプト実行時には、AddHostObject メソッドで C# のオブジェクトを JavaScript 側に公開しています。
engine.AddHostObject("context", model.Context);
engine.AddHostObject("model", model.Model);
engine.AddHostObject("items", model.Items);
engine.AddHostObject("columns", model.Columns);
// ... 合計 19 種のホストオブジェクト
この AddHostObject で渡されたオブジェクトだけが JavaScript から見える世界です。model.ClassA = 'test' のような値操作はできますが、それ以外の .NET の機能には一切触れられません。
なぜ ClearScript(V8)が採用されているのか
ClearScript が採用されている大きな理由の一つは V8 エンジンが OS レベルの操作手段を一切持たない 点にあります。
V8 はもともとブラウザ用の JavaScript エンジンです。ブラウザの中で安全に動くことが大前提なので、ファイルシステムやプロセスの起動といった OS 操作の API がエンジン自体に存在しません。
| 操作 | V8 (ClearScript) |
|---|---|
| プロセス起動 | 不可能(API がない) |
| ファイル操作 | 不可能(API がない) |
| ネットワーク接続 | 不可能(API がない) |
| 環境変数アクセス | 不可能(API がない) |
| OS シャットダウン | 不可能(API がない) |
Node.js では fs モジュールや child_process モジュールでこれらの操作ができますが、それは Node.js が独自に追加した API であって V8 エンジン自体の機能ではありません。
つまり ClearScript で V8 を使う場合、AddHostObject で明示的に渡したオブジェクト以外には原理的にアクセスできないのです。サンドボックスが「設計上無料で」手に入るわけですね。
V8(ClearScript)のセキュリティモデル
-
AddHostObjectで公開したオブジェクトのみ見える - OS API = 存在しない ← エンジンに API が無い(原理的保証)
Python でも書きたい
JavaScript でサーバスクリプトが書けるのは便利ですが、Python の方が得意だったり、Python の豊富な文字列操作や datetime の扱いやすさを活かしたいケースもあります。
そこで .NET 上で Python を実行する方法を調べてみました。
候補ライブラリの比較
.NET で Python スクリプトを実行する主な方法は2つあります。
| 項目 | IronPython 3 | Python.NET (pythonnet) |
|---|---|---|
| 方式 | .NET 純粋実装の Python | CPython を .NET に埋め込み |
| NuGet パッケージ |
IronPython (3.4.x) |
pythonnet (3.0.x) |
| Python 互換性 | Python 3.4 相当 | CPython 完全互換 |
| 外部依存 | なし(NuGet のみ) | CPython のインストールが必須 |
| .NET 連携 | DLR ベースで直接連携 | 型変換が必要 |
| NumPy / Pandas | 利用不可 | 利用可能 |
| サンドボックス | 4層ロックダウンで実現可能 | 制限が困難 |
| スレッドセーフ | スコープ分離で安全 | GIL による制約あり |
| Docker 対応 | 追加インストール不要 | Python ランタイム必要 |
| ライセンス | Apache 2.0 | MIT |
IronPython を推す理由
サーバスクリプトに Python を追加するなら IronPython 3 が適しています。理由を整理すると以下の通りです。
デプロイが簡単
NuGet パッケージを追加するだけで完結します。CPython のインストールは不要で、Docker イメージの変更も不要です。プリザンターは Docker 環境での運用も多いため、これは大きなメリットです。
.NET との親和性が高い
IronPython は .NET の DLR(Dynamic Language Runtime) 上で動作します。ClearScript が AddHostObject で C# オブジェクトを公開するのと同じように、IronPython では scope.SetVariable() で同じことができます。
// ClearScript (JavaScript)
engine.AddHostObject("model", expandoObject);
// IronPython (Python)
scope.SetVariable("model", expandoObject);
C# の ExpandoObject を Python から直接操作できるため、既存のホストオブジェクト(model, context, items 等)をそのまま流用できます。
サーバスクリプトで使わない機能は不要
Python.NET なら NumPy や Pandas が使えますが、サーバスクリプトの用途はレコードの値操作、条件分岐、API 呼び出しが中心です。科学計算ライブラリは必要ありません。
IronPython とは
IronPython は Python 言語の .NET 実装です。CPython(通常の Python)とは別のエンジンで、.NET の DLR 上で Python コードをコンパイル・実行します。
// IronPython の基本的な使い方
using IronPython.Hosting;
var engine = Python.CreateEngine();
var scope = engine.CreateScope();
// C# のオブジェクトを Python に公開
scope.SetVariable("context", serverScriptModel.Context);
scope.SetVariable("model", serverScriptModel.Model);
// Python スクリプトを実行
engine.Execute("model.ClassA = 'updated by Python'", scope);
Python から見たスクリプトの書き方はこんなイメージです。
# サーバスクリプト(Python版のイメージ)
if context.Action == 'create':
model.ClassA = f"作成者: {context.UserName}"
model.NumA = model.NumB * 1.1
import json
data = json.loads(model.DescriptionA)
model.ClassB = data.get('category', '未分類')
import datetime
today = datetime.date.today()
model.DateA = today.isoformat()
JavaScript 版と比較してみましょう。
// サーバスクリプト(JavaScript版)
if (context.Action === 'create') {
model.ClassA = `作成者: ${context.UserName}`;
model.NumA = model.NumB * 1.1;
}
const data = JSON.parse(model.DescriptionA);
model.ClassB = data.category || '未分類';
const today = new Date();
model.DateA = today.toISOString().split('T')[0];
やっていることは同じですが、Python の方が読みやすいと感じる方もいるのではないでしょうか。
セキュリティの課題:OS に触れてしまう問題
ここで重要な問題があります。IronPython は DLR(.NET)上で動作するため、何も制限しなければ .NET の全機能にアクセスできてしまうのです。
# こんなことが出来てしまう(制限なしの場合)
import clr
clr.AddReference("System")
from System.Diagnostics import Process
Process.Start("cmd", "/c whoami") # OS コマンド実行!
from System.IO import File
content = File.ReadAllText("C:\\secrets.txt") # ファイル読み取り!
import os
os.system("shutdown /s") # OS シャットダウン!
ClearScript(V8)では「API がないからそもそも不可能」だったのに対し、IronPython では 「API が豊富にあるから制限が必要」という真逆のアプローチになります。
V8 (ClearScript) — 原理的保証 → API が存在しないから安全
IronPython (DLR/.NET) — 実装的保証 → API はあるが封鎖して安全にする
| 操作 | V8 (ClearScript) | IronPython (制限なし) |
|---|---|---|
| プロセス起動 | 不可能(API なし) |
Process.Start() で可能 |
| ファイル操作 | 不可能(API なし) |
System.IO.File で可能 |
| ネットワーク接続 | 不可能(API なし) |
socket / System.Net で可能 |
| 環境変数アクセス | 不可能(API なし) |
os.environ で可能 |
サンドボックスで解決する
「じゃあやっぱり無理では?」と思われるかもしれませんが、IronPython でも適切なサンドボックスを構築すれば V8 と同等の安全性を実現できます。
設計原則は明確です。JavaScript と同じこと(ホストオブジェクト経由の値操作)を Python 構文で書けるようにする。Python の汎用プログラミング機能(ファイル操作・ネットワーク・OS 制御等)は提供しない。
4層防御(Defense in Depth)
サンドボックスは1つの仕組みだけに頼らず、4つの防御レイヤーを重ねて構築します。
| レイヤー | 防御対象 | 方法 |
|---|---|---|
| Layer 1 | 危険な組み込み関数 |
__builtins__ のホワイトリスト化(open, exec, eval 等を除去) |
| Layer 2 | 危険なモジュール | カスタム __import__ で math, json, datetime, re 等のみ許可 |
| Layer 3 | 外部 .py ファイル |
SearchPaths を空にして物理ファイルの読み込みを遮断 |
| Layer 4 | .NET アセンブリ |
clr, System, Microsoft 等のモジュールを sys.modules で封鎖 |
ロックダウン後の世界
4層のロックダウンを適用した後、Python スクリプトから見える世界は以下の3つだけです。
| カテゴリ | 内容 | 例 |
|---|---|---|
| ホストオブジェクト | C# から注入された 19 種のオブジェクト |
model, context, items, columns
|
| 安全な builtins | 副作用のない組み込み関数 |
len, str, int, list, range, sorted
|
| 安全なモジュール | 純粋演算の標準モジュール |
math, json, datetime, re
|
IronPython(サンドボックス適用後)
-
SetVariableで公開したオブジェクトのみ見える - OS API = 封鎖済み ← 4層防御で遮断(実装的保証)
V8 が「原理的にアクセス不可能」なのに対し、IronPython では「原理的にはアクセス可能だが実装的に封鎖」という違いがあります。そのため、継続的な脆弱性検証は必要ですが、実用上は同等の安全性が実現可能です。
具体的にどう封鎖するのか
例えば import os をブロックする仕組みはこのようになります。
// カスタム __import__ でホワイトリスト方式のインポート制御
var allowedModules = new HashSet<string>
{
"math", "json", "datetime", "re", "decimal",
"string", "collections", "itertools", "functools"
};
// ホワイトリストに無いモジュールはインポート拒否
// import os → ImportError: Module 'os' is not allowed
// import subprocess → ImportError: Module 'subprocess' is not allowed
// import math → OK(ホワイトリストに含まれている)
open() や exec() の封鎖は __builtins__ の差し替えで実現します。
// 安全な builtins のみを残し、危険な関数を除去
// 許可: len, str, int, float, list, range, sorted, ...
// 除去: open, exec, eval, compile, exit, __import__, ...
サンドボックスの詳細な実装設計は後編で解説します。
実装時のアーキテクチャ
Python 対応を実装する場合、既存の ScriptEngine クラス(V8 ラッパー)と同等の PythonScriptEngine を新設し、共通インターフェースで抽象化します。
スクリプトごとに言語を選択できるようにし、サイト単位でのデフォルト言語設定も追加します。既存のスクリプトは Language プロパティが未指定 (null) の場合に JavaScript として動作するため、後方互換性は完全に維持されます。
実現可能性の評価
調査の結果を総合すると、IronPython による Python 対応は 実現可能性が高い と判断できます。
| 評価項目 | 結果 |
|---|---|
| .NET オブジェクト連携 | DLR ベースのため ClearScript と同等に連携可能 |
ExpandoObject 操作 |
Python から直接プロパティアクセスが可能 |
| タイムアウト制御 |
sys.settrace コールバックで実装可能 |
| サンドボックス | 4層防御で V8 同等の分離レベルを実現可能 |
| デプロイ | NuGet パッケージ追加のみ、Docker 変更不要 |
| 後方互換性 | デフォルト JavaScript のため既存環境に影響なし |
主な技術的リスク
| リスク | 影響度 | 対策 |
|---|---|---|
| サンドボックス漏れ | 高 | 4層ロックダウン + 継続的な脆弱性検証 |
| タイムアウト制御 | 中 |
sys.settrace + 無限ループの検証 |
ExpandoObject 連携 |
低 | DLR ベースのため基本的に互換(POC で検証) |
一番の課題はやはりサンドボックスです。V8 とは異なり「原理的保証」ではなく「実装的保証」になるため、テストの徹底と継続的な脆弱性チェックが欠かせません。
まとめ
- プリザンターのサーバスクリプトは ClearScript(V8 エンジン) で動作しています
- ClearScript が採用されている理由の一つは、V8 が OS レベルの操作手段を原理的に持たないため安全なサンドボックスが「無料で」手に入る点にあります
- Python 対応には IronPython 3 が最適で、NuGet パッケージの追加だけでデプロイでき、.NET オブジェクトとの連携も ClearScript と同等にスムーズです
- IronPython は .NET 全体にアクセスできてしまうリスクがありますが、4層防御のサンドボックスで V8 同等の安全性を実現できます
- 後方互換性を維持した設計で、既存環境に影響を与えずに段階的に導入できます
後編では、4層防御のサンドボックスの具体的な設計と実装方法を詳しく解説します。