BedderScript security model
Audited 2026-08-15 (line-by-line review of the escape surface + DoS hardening pass with battery proof). This file states what is guaranteed, what is the embedder's job, and what a hostile script can still do.
Threat model
The adversary is a hostile script — a mod, a player-made level, pasted code. The script is untrusted; the embedding application and its host implementation are trusted.
Scope note (2026-08-15): everything below concerns the script sandbox. The scene-op booklet and the asset catalog are a different, wider trust tier — see "Scene mods and assets" at the end. Statements like "no reflection on script input" are about scripts, and remain true; they are deliberately NOT true of booklets.
Verified: no escape surface
A script cannot reach the CLR, the filesystem, the network, or arbitrary Unity API:
- No reflection anywhere on script input.
GetProperty/SetPropertyare closed switches (name/tag/active/x/y/z — that is the entire set).CallMethodon the Unity host answers one verb. Component types resolve through an approved-name table, neverType.GetType. - No eval, no IL, no codegen — source compiles once to bytecode for a closed-opcode VM.
- No IO capability exists to misuse:
save_*is an in-memory dictionary owned by the host; there is no file, network, process, or thread API in the language. - Walking an exposed object into CLR metadata (
.GetType().Assemblyand friends) is impossible by construction, not by configuration — there is no member-access path from a script value to CLR metadata.
Enforced limits (each has a battery test where it matters)
| Limit | Enforced at | Notes |
|---|---|---|
| MaxInstructionsPerFrame / PerCall | every opcode dispatch | |
| MaxExecutionSeconds | every dispatch (wall clock, per outermost invocation) | |
| MaxRecursionDepth / MaxStackDepth | frame push / stack push | script recursion |
| MaxEventChainDepth | emit | |
| MaxCollectionSize | array/map growth + indexing | |
| MaxScriptMemoryBytes | string concat, join, replace (worst-case pre-charge) |
allocation budget per invocation — resets like the instruction budget |
| MaxSpawnedObjects | spawn builtin |
cumulative per VM session |
| MaxCoroutineCount | coroutine create | |
| Parser nesting (fixed, 200) | Expression/Statement/Unary |
a compile-time bomb (thousands of nested parens / unary chains) is a diagnostic, not a process-killing StackOverflow |
Worst case for a hostile script: it hits a limit and its invocation stops with an error the host sees. (As of 2026-08-15 this sentence is true for every limit in the table.)
The embedder's duties (holes only YOU can open)
- ModuleResolver — you map module names to source. Never map names to file paths without
canonicalization;
importnames are attacker-chosen strings (../../secretsmust not work because of YOUR code, not ours). - RegisterApi / custom hosts — every function you register is capability you grant. The sandbox's whole guarantee is the sum of what the host exposes. No reflection-based auto-binding; keep it that way.
- Scene authority — the default Unity host gives scripts whole-scene reach (
find+destroy/set_positionon anything nameable). That is machine-safe but game-integrity- trusting. If mods should not grief the scene, scope a host (ExtensibleEngineHost) that resolves names against an allow-list instead. - Source size — cap script file size at your load door; the parser is robust but a 100 MB source string still costs memory to lex.
Known residual risks (accepted, documented)
- Host-call wall time is not metered per call — a script may call
find()(a scene scan) thousands of times within its instruction budget. Bounded by MaxInstructionsPerFrame; a per-frame host-call budget is the growth path if a real workload needs it. - The memory budget is allocation accounting at the known amplification points (concat, join, replace), not a GC-integrated heap meter. Amplifiers are pre-charged with worst-case estimates; steady accumulation across frames is bounded by MaxCollectionSize.
Scene mods and assets — the LOCAL trust tier (added with the scene-op spine)
The booklet (Documents/<product>/SceneMods/<scene>.json) and the asset catalog
(Documents/<product>/Assets/) are the player modding their own machine — the same trust
tier as editing a save file. Within that tier, deliberately wider powers than scripts get:
add_component/set_propertyops resolve any loaded component type by name and set members by reflection (the in-game inspector needs full reach). This is the local tier working as designed.- Asset files pass an extension allow-list (image/audio/model only — never code), a 50 MB cap, and are sha-deduped copies; importer parse failures are caught, never fatal.
- OBJ/MTL/GLB parsing and
Texture2D.LoadImagerun on user bytes — malformed files fail the import, they do not execute.
NOT YET HARDENED — do not ship booklet/asset SHARING without this: a booklet downloaded
from another player is not save-file-tier, it is hostile-input-tier. Before any sharing
feature: (1) an op allow-list mode for add_component/set_property (curated types + members,
like the script-side table), (2) reject catalog fileName entries containing path separators
at load, (3) cap booklet op count per file. Until then, the only supported booklet source is
the local player's own machine.
Source in the box: Assets/ArTchie Studios/ArTchie-Bedder-Suite/com.bedder.script/SECURITY.md