Skip to content

サーバースクリプトを Python で書けるようにする ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.1.01.5.8.1

1.5.8.1 のプリザンターでは、サーバースクリプトは ClearScript(V8)で動く JavaScript だけです。このページは、サーバースクリプトに Python を追加する本体改修の設計メモで、本体の標準機能ではありません。調査は 1.5.1.0 を対象に行い、前提にした現行実装は 1.5.8.1 のソースで確かめ直しています。

概要と試作の流れは C# スクリプト・Python は使えるか にあります。このページはその詳細設計です。

前提にした現行実装 ​

  • エンジンは ScriptEngine クラスで、V8ScriptEngine を薄くラップしたものです。リクエストのたびに作って破棄します(ScriptEngine.cs、ServerScriptUtilities.cs#L1217)。
  • スクリプトに見えるのは、AddHostObject で渡す 19 個のオブジェクト(context・grid・model・saved・depts・groups・users・columns・siteSettings・view・items・hidden・responses・elements・extendedSql・notifications・httpClient・utilities・logs)と、AddHostType で渡す Newtonsoft.Json.JsonConvert だけです(ServerScriptUtilities.cs#L1217-L1270)。
  • 共有スクリプトと対象スクリプトは、改行で連結して 1 回で実行されます。
  • ServerScript に言語を表すプロパティはありません(ServerScript.cs#L7-L35)。サイトのダイアログのコードエディタも dataLang: "javascript" 固定です(SiteUtilities.cs#L17983)。

ClearScript に直接依存している箇所 ​

Microsoft.ClearScript を参照しているのは次の 5 ファイルです。別のエンジンを足すときはここが改修対象になります。

ファイル依存内容
Libraries/ServerScripts/ScriptEngine.csV8ScriptEngine・V8ScriptEngineFlags
Libraries/ServerScripts/ServerScriptFile.cs引数の ScriptObject callback、戻り値の V8ScriptEngine.Current.Script.Array.from()(#L25、#L98)
Libraries/ServerScripts/ServerScriptCsv.cs同上(#L14、#L59-L61)
Libraries/ServerScripts/FormulaServerScriptUtilities.csScriptEngineException の捕捉(#L98)
Filters/HandleErrorExAttribute.csScriptEngineException の判定(#L49)

加えて ServerScriptJsLibraries.cs は $ps などを組み立てる JavaScript のコードそのもので、Python 用には別に用意する必要があります。

設計原則:値操作だけを許す ​

V8 はファイル・プロセス・ネットワーク・環境変数を操作する API をエンジン自体が持たないため、AddHostObject で渡したもの以外には原理的に触れません。IronPython は .NET の DLR 上で動くため、何もしなければ import clr から .NET の全機能に届きます。

そこで Python 版の目的を「JavaScript と同じこと(ホストオブジェクト経由の値操作)を Python の構文で書けるようにする」に限定し、Python の汎用機能(ファイル・ネットワーク・OS 操作)は提供しません。

許可する例
ホストオブジェクトの読み書き・メソッド呼び出しmodel.ClassA = 'test'、items.Get(123)、context.ErrorData.Type = 1
Python の基本構文条件分岐・ループ・内包表記・文字列操作・算術
副作用のない組み込み関数len、str、int、list、range、sorted など
純粋演算の標準モジュールmath、json、datetime、re、decimal、collections など
禁止する例
.NET 相互運用import clr、import System
OS・プロセスos、subprocess、ctypes、threading、multiprocessing
ファイル・ネットワークopen()、io、pathlib、socket、urllib、http
動的なコード実行・読み込みexec、eval、compile、__import__、importlib
任意コード実行につながる直列化pickle、shelve、marshal

V8 の「原理的保証」に対して、IronPython は封鎖による「実装的保証」になります。この差がこの改修の最大のリスクです。

ライブラリの選定 ​

項目IronPython 3Python.NET(pythonnet)
方式.NET で実装した PythonCPython を .NET プロセスに埋め込む
NuGetIronPython(3.4.x)pythonnet(3.0.x)
言語の互換性Python 3.4 相当(一部 3.6 以降の構文も可)CPython と完全互換
外部依存なしサーバーに CPython のインストールが必要
.NET オブジェクトとの連携scope.SetVariable() でそのまま渡せる変換が必要
NumPy などの C 拡張使えない使える
サンドボックス封鎖すれば実現できる制限が難しい
スレッドスコープを分ければ安全GIL の制約がある
ライセンスApache 2.0MIT

IronPython 3 を採用します。NuGet の追加だけで済み(Docker イメージも変えなくてよい)、ClearScript の AddHostObject と同じ感覚で C# のオブジェクトを渡せ、ExpandoObject を Python から model.ClassA の形で読み書きできます。サーバースクリプトの用途に NumPy などは要りません。

改修の構成 ​

IScriptEngine を抽出し、既存の V8 ラッパーと新しい PythonScriptEngine をファクトリで切り替えます。

図を読み込み中…

層ファイル内容
エンジンScriptEngine.csIScriptEngine を抽出し、既存クラスを V8 用の実装にする
エンジン新規 IScriptEngine.cs・PythonScriptEngine.cs・ScriptEngineFactory.csインターフェース、IronPython の実装、言語に応じた生成
データServerScript.csLanguage(int?)を追加
データSiteSettings.csサイトの既定言語 ServerScriptLanguage(int?)を追加
実行ServerScriptUtilities.csExecute でファクトリからエンジンを作る。言語ごとに分けて実行する
実行新規 ServerScriptPyLibraries.cs$ps 相当の Python 用ヘルパー
依存の解消ServerScriptFile.cs・ServerScriptCsv.csScriptObject のコールバックと JS 配列の戻り値をやめる
画面SiteUtilities.csダイアログに言語の選択を追加し、コードエディタの dataLang を切り替える。サイトの既定言語の設定も追加

FormulaServerScriptUtilities(計算式のスクリプト)とバックグラウンドサーバースクリプト(TenantUtilities.cs)は、最初は JavaScript のままにします。

言語の決め方 ​

図を読み込み中…

Language を int? にするのは、既存の設定 JSON に値が無い(null)とき JavaScript として動かすためです。0 が JavaScript、1 が Python です。

json
{
    "ServerScriptLanguage": 0,
    "ServerScripts": [
        { "Id": 1, "Title": "BeforeCreate (JS)", "Body": "model.ClassA = 'test';", "BeforeCreate": true, "Language": 0 },
        { "Id": 2, "Title": "BeforeUpdate (Python)", "Body": "model.ClassA = 'updated'", "BeforeUpdate": true, "Language": 1 }
    ]
}

同じ条件に 2 つの言語があるとき ​

現行はスクリプトを連結して 1 回で実行するので、言語が混ざると 1 つのエンジンでは動きません。候補は 3 つです。

方式内容評価
言語ごとに順に実行JavaScript をまとめて実行し、次に Python をまとめて実行する採用(現行の「連結して一度に実行」に近い)
登録順に実行Id 順に言語を切り替えながら実行する見送り
混在を禁止同じ条件で異なる言語を登録できなくする次点

どちらのエンジンも同じ ServerScriptModel のオブジェクトを受け取り、最後に SetValues() で画面・レコードに反映します。

File・CSV の ClearScript 依存を外す ​

ServerScriptFile・ServerScriptCsv のメソッドは、エラー通知に ClearScript の ScriptObject を受け取り、戻り値を V8ScriptEngine.Current で JS 配列にしています。

  • 方式 A(採用): コールバックを Action<string, string> などの .NET のデリゲートに変え、JavaScript 側・Python 側それぞれのラッパーで例外に変換する。
  • 方式 B: 戻り値を List<List<string>> などの .NET 型にそろえ、各言語のラッパーで配列に変換する。

PythonScriptEngine の要点 ​

  • コンストラクタで Python.CreateEngine() し、ユーザーのスクリプトより前にサンドボックスを適用してからスコープを作ります。
  • AddHostObject は scope.SetVariable(name, target)、Execute は SourceCodeKind.Statements、Evaluate は SourceCodeKind.Expression で実行します。
  • Dispose で engine.Runtime.Shutdown() します。
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', '未分類')

タイムアウト ​

ClearScript は ContinuationCallback で V8 の実行に割り込んでタイムアウトを判定しています。IronPython では次の方式を比べ、sys.settrace を採用します。

方式評価
別スレッドからエンジンを止める(Thread.Abort など)危険。.NET では Thread.Abort が使えない
sys.settrace で文ごとにコールバックし、期限を過ぎたら TimeoutError を投げる採用。無限ループも止められる
Task.Run と CancellationTokenCPU だけを使うループは止められない

トレース関数は、サンドボックスで sys を封じる前に設定しておく必要があります。

サンドボックス(4 層の防御) ​

1 つの仕組みでは抜け道が残るため、4 つを重ねます。

図を読み込み中…

Layer 1:組み込み関数の制限 ​

スコープの __builtins__ を、許可した関数だけの辞書(PythonDictionary)に差し替えます。IronPython 3 では exec・eval は文ではなく関数なので、辞書から外せば使えなくなります。

外すもの理由
__import__・exec・eval・compile任意のコード実行・モジュール読み込み(__import__ は Layer 2 の版に置き換える)
open・inputファイル I/O・標準入力
exit・quit・breakpoint・helpプロセス終了・デバッガ・ヘルプ経由の情報
globals・locals・varsスコープの辞書からサンドボックスの設定を書き換えられる
getattr・setattr・delattr・type属性の動的アクセスやメタクラス操作で制限を回避できる(外すのを推奨)

残すのは型変換(int・str・list・dict など)、数値(abs・round・min・max・sum など)、反復(range・len・enumerate・zip・map・filter・sorted など)、文字列(chr・ord・format など)、isinstance・hasattr など安全なもの、例外クラス、True・False・None です。print はログ用に残すかを選べます。

Layer 2:import の制御 ​

__import__ を、許可リストに無いモジュールで ImportError を投げる関数に差し替えます。この方式が一番効果があり、ビルトインモジュールにもファイルのモジュールにも効きます。

許可するモジュールの例は json・math・cmath・datetime・re・collections・itertools・functools・operator・string・decimal・fractions・copy・enum・typing・abc・dataclasses・textwrap・unicodedata・base64・hashlib・uuid・random・statistics・bisect・heapq・calendar などです。IronPython 自体が内部で使う _collections・_functools・_sre・codecs なども必要になります。

方式ビルトインモジュールファイルのモジュール回避されにくさ
カスタム __import__防げる防げる高い
PlatformAdaptationLayer のサブクラス防げない防げる高い
CreateEngine のオプション防げない防げない-
sys.modules の操作防げる防げる低い(del sys.modules['os'] で外せる)

CreateEngine のオプション(Debug・Frames・LightweightScopes など)にサンドボックスの機能は無く、CLR 相互運用を切る公式の NoClr のようなオプションもありません。

Layer 3:ファイルからの読み込みを塞ぐ ​

engine.SetSearchPaths() を空にし、さらに PlatformAdaptationLayer のサブクラス(FileExists が常に false、OpenInputFileStream が例外)を使うと、.py ファイルの読み込みを止められます。ただし os のようなビルトインモジュール(C# 実装)はファイルシステムを通らずに読み込まれるので、Layer 2 と組み合わせる必要があります。

Layer 4:.NET 相互運用とモジュールの封鎖 ​

sys.modules['clr'] などを None にしておくと、import が ImportError になります(モジュールポイズニング)。

区分封鎖するモジュール
必須os・subprocess・sys・ctypes・socket・importlib・shutil・io
強く推奨multiprocessing・threading・_thread・signal・tempfile・pathlib・glob・pickle・shelve・dbm・sqlite3
推奨http・urllib・webbrowser・ftplib・smtplib・poplib・imaplib・xml・zipfile・tarfile・gzip・bz2・lzma
IronPython 固有clr・System・Microsoft・nt・posix・_io・_socket・_ssl
調査・デバッグ用inspect・dis・code・codeop・pdb・trace など

clr が読み込めると clr.AddReference() で任意のアセンブリを読み込め、System.Diagnostics.Process や System.IO.File に届きます。.NET Framework 時代の Code Access Security(CAS)は .NET Core 以降で廃止されており、.NET 10 のプリザンターではランタイムに任せられるサンドボックスはありません。すべてアプリケーション側で作ります。

封鎖したいアクセスの経路

図を読み込み中…

ホストオブジェクトからの抜け道 ​

clr を封じても、注入した C# のオブジェクトの .GetType() からリフレクションに進める可能性があります。対策として、注入時に Process・File・Assembly などの型を渡していないか確認する、DynamicObject を継承したラッパー(GetType などを拒否する)で包む、が考えられます。ラッパーは性能に影響するので、ExpandoObject ではない .NET の型のインスタンス(httpClient など)を渡すときに限って検討します。

残るリスク ​

リスク内容対策
object.__subclasses__()().__class__.__bases__[0].__subclasses__() で全サブクラスから危険な型を探すtype・getattr を外し、__subclasses__・__bases__・__mro__ へのアクセスを制限する。IronPython では返る型が CPython と違うが、.NET の型が含まれうるので対策は必要
CPU の枯渇while True: passsys.settrace によるタイムアウト
メモリの枯渇[0] * 10**9IronPython にメモリ制限の機能は無い。コンテナのメモリ上限、GC.GetTotalMemory() の監視、別プロセスでの実行

確認するテスト ​

スクリプト期待結果
import os・import subprocess・import socket・import pickleImportError
import clr・import System・clr.AddReference(...) 経由の ProcessImportError
import importlib; importlib.import_module('os')ImportError
open(...)・exec(...)・eval(...)・compile(...)・exit()NameError などの例外
import math; math.sqrt(16)4.0
import json; json.dumps({'a': 1})正常
model.ClassA = 'test'・items.Get(123)正常
__subclasses__() を使ったエスケープ失敗すること

本番に出す前に、IronPython 3.4.x で上のテストを全部実行して挙動を確かめること、コンテナのメモリ上限とスクリプトのエラーログを用意することが前提です。

評価 ​

項目内容
実現可能性高い。既存の ScriptEngine と同じ形で実装できる
後方互換Language が null なら JavaScript。既存データに影響しない
CodeDefiner言語の判定は実行時なので、自動生成のテンプレートは変えなくてよい
主なリスクサンドボックスが実装的保証であること。テストと脆弱性の継続的な確認が要る
フェーズ内容工数の目安(人日)
1. 基盤IScriptEngine の抽出、V8 ラッパーへの改名、ファクトリ(動作は変わらない)2.5
2. エンジンPythonScriptEngine、タイムアウト、サンドボックス、Language、Execute の言語分岐8
3. 画面と依存の解消言語の選択、エディタの切り替え、サイトの既定言語、File・CSV の ClearScript 依存除去、Python 用ヘルパー5
4. テストJavaScript の回帰、Python、サンドボックス突破5
将来計算式・バックグラウンドサーバースクリプトの Python 対応、サンプル集-

合計は約 20.5 人日(調査時の概算)です。

関連ページ ​

変更履歴

第1版バックグラウンドサーバースクリプトの仕組みと、サーバースクリプトの他言語対応・DLL 実行・cron スケジュールの改修・設計メモを追加