Security Guidance for Authors
When you ship a Lua script in a world, an avatar, or a prop, you take responsibility for the security of every player who interacts with it. CVR runs Lua client-side under MoonSharp, with access gated by a four-axis policy (see Context & Permissions). The runtime stops a script from breaking out of the sandbox; it does not stop your design from leaking secrets or trusting input it shouldn’t.
The threat model in one paragraph
Section titled “The threat model in one paragraph”Every byte of your script reaches every client that loads your content, plain-text. Every variable lives in client memory the player owns. The player can read it, modify it, replace it, or fake the network messages your script sends. CVR’s network layer authenticates the transport, not the content of any individual script message. Your script must defend itself.
Three rules every author should internalize
Section titled “Three rules every author should internalize”- Never trust the client. Anything a client reports — input, state, network message, animator parameter — can be forged. Validate before acting. See Rule 0 below for a concrete worked example.
- Keep the smallest scope possible. Other scripts in the same content root can read and call your global functions and variables. Use
localaggressively. The runtime does not isolate scripts from each other within one content root. See Rule 1 below. - Match the gate to the action. A purely cosmetic effect (rotate a cube) needs no validation. A gate that opens a door deserves a server-blessed token. A grant that costs a player something needs an authoritative source — usually the instance owner — making the call. The Lua runtime cannot tell those three apart; you do, in code.
When you need a permission gate
Section titled “When you need a permission gate”These are the load-bearing checks every author should think through before publishing:
- Identity-bearing actions (whitelist, ban list, “owner can do X”): use
Player.GetUserId()(requires the world’sAccessUserIdentitypermission) — never user-supplied display names. Display names are not unique and can be spoofed. - Persistence reads/writes (saving progress, score, inventory): reads of public bins are open; writes need the right scope (Self / Owner / Any). Cross-check the gate against Context & Permissions before adding write paths to data that affects other players.
- Networked commands (move other players, force animations, give items): the receiver must validate the sender. A message claiming to come from the instance owner must be checked against
Players.GetInstanceOwner(). The sender id in the message envelope is forgeable; treat it as a hint, not proof. - Outbound HTTP / external services: gated by a separate world permission. Don’t pass user-controlled strings into URLs without sanitizing — a malicious player can craft input that turns into an unintended request.
What never goes in a script
Section titled “What never goes in a script”- Hard-coded secrets — keys, tokens, passwords, voice-codec API keys. Anyone who opens the .unitypackage with Unity Explorer reads them.
- Obfuscated secrets. Rule 0 below shows why this fails to even slow an attacker down.
- Anything you’d be embarrassed by if it leaked. Including angry comments about other content authors, hidden Easter eggs that doxx players, or dev-mode flags you forgot to turn off.
What does work
Section titled “What does work”- Cryptographic hashes for “did the player type the right code” — server learns nothing; the player’s input is hashed and compared. Rule 0’s “Secure Way” section walks through it.
- Owner-authoritative state — only the instance owner mutates the canonical state and broadcasts updates; non-owner messages are ignored. This is how every networked CVR pattern that survives contact with bad actors works.
- Small, well-scoped messages — the smaller the payload schema, the smaller the attack surface. A message format with one byte of “tag” and a fixed body is much easier to validate than a free-form string.
- Defensive timeouts and rate limits — a hostile client can spam any callable function. Cap how often any single player can trigger expensive operations.
Specific guidelines
Section titled “Specific guidelines”The rest of this page is specific patterns, with worked examples.
Rule 0. Never Trust The Client
Section titled “Rule 0. Never Trust The Client”This is the most important lesson you must learn.
Here’s a frequent use-case: An access keypad.

The Wrong Way
Section titled “The Wrong Way”The easiest way to handle this would be to just throw the key into the script as a constant.
KEYCODE = '1234'-- Start up and blank out the keypad.function Start() boundObjects["Text"].text = ""end
-- Receive key press from individual keysfunction Receive(key) local t = boundObjects["Text"] -- The clear key if key == "C" then t.text = "" boundObjects["DoorHandle"].enable=false return end -- If enter key and the text is our keycode if key == "E" and t.text == KEYCODE then -- Open door boundObjects["DoorHandle"].enable=true t.text = "" return end t.text = t.text .. keyendUnfortunately, anyone with Unity Explorer can pop open the script and see the KEYCODE variable.
Slightly Less Wrong Way
Section titled “Slightly Less Wrong Way”Another way of handling this would be to obfuscate things. Below is the same KEYCODE, but obfuscated.
KEYCODE = ((0x61e3d0d5/0x5192d6b+2.7755575615629e-17)+(0x1f694311/0x1955b30+-1.1102230246252e-16)+(0x16c0824/0x10b4595)+(0x6a60bf/0x6ff17e+6.6613381477509e-16)+(0x380f46a/0x21b7965+1.1102230246252e-16)+(0x1c50fa7c/0x2d1fded+-9.7144514654701e-17)+(0x114e7e9c/0xbc7a13+4.4408920985006e-16)+(0x1927996/0x1dc8a73+1.1102230246252e-16)+(0x10e9a2c9/0x1b385ba+-5.5511151231258e-16)+(0x11c04e45/0x1bbe5a6+4.1633363423443e-16)+(0x6d89129/0x188efea+5.5511151231258e-16)+(0x289c9e08/0x2b2ac3d+-1.5959455978987e-16)+(0x21401c1/0x56850a+2.7755575615629e-16)+(0x1c7febb5/0x1681ab4+-2.2204460492503e-16)+(0xfed92e/0x17e6bb+-8.8817841970013e-16)+(0x2d1a1d7b/0x2e0f894+2.2204460492503e-16)+(0x7fa3687/0x1667514+-3.3306690738755e-16)+(0x75867d1/0xc44d2d+8.8817841970013e-16)+(0x1d394686/0x1343e31+2.2204460492503e-16)+(0x2d16a30f/0x1402dbfa)+(0xe99a123d/0x8c36f46)+(0x2afc565b/0x30ef60e)+(0x220ec4eb/0x12bba2b+9.1593399531575e-16)+(0x1441e017/0x1611f09+-6.6613381477509e-16)+(0x368d4fcb/0x1fe2618+4.9960036108132e-16)+(0x4d666d3/0x1ea4914+-1.1102230246252e-16)+(0x27717f27/0x1938233+-2.0816681711722e-17)+(0x69f2384/0x3df46cd+1.1102230246252e-16)+(0x1719ad60/0x1e8c151+5.5511151231258e-17)+(0xa420faa/0x17697d7+-7.3378803033819e-16)+(0x38144896/0x289aca1+-1.6653345369377e-16)+(0x54b99a08/0x2d6ee39+2.2204460492503e-16)+(0xfe1b5af/0x351391f)+(0x42849bd6/0x2499517+1.8041124150159e-16)+(0x239166b6/0x209c123+-2.2204460492503e-16)+(0xdd31a45/0x135f00e+2.2204460492503e-16)+(0x2306c475/0x1d6050a+9.7144514654701e-17)+(0x30f26a23/0x260c4f0+-1.1102230246252e-16)+(0x134fad1d/0x2c28044+-2.2204460492503e-16)+(0x27088b5b/0x32d2eb1+-5.5511151231258e-17)+(0xf690566/0xf2128d+-7.7715611723761e-16)+(0x6e85671/0x1fc82d5+-3.3306690738755e-16)+(0xe8b82bd/0xd9c032+-3.7470027081099e-16)+(0x7166f56/0x23c6253+-3.6082248300318e-16)+(0x140b07c/0xadfc3f+4.4408920985006e-16)+(0x174c18ef/0x16d1670+-4.9960036108132e-16)+(0x1a735971/0x1010ea5+-4.9960036108132e-16)+(0x2185f975/0x410c927+-5.5511151231258e-17)+(0x29a2e94b/0x3f3a200+1.1102230246252e-16)+(0x10d3c919/0x1bf439c+1.1102230246252e-16)+(0x36d21f18/0x214764f+-3.885780586188e-16)+(0x1e1dda01/0x28e4fb5+-2.2204460492503e-16)+(0x4ae8b675/0x2870ffa)+(0x405a5c1f/0x2306cdc+2.2204460492503e-16)+(0x30113425/0x2b36963+-1.1102230246252e-16)+(0xc612a9c/0x12ae5ab+-3.3306690738755e-16)+(0x5cf98f0e/0x347d769)+(0xd34634f/0x1ae1810)+(0x107b30d9/0x26e34e4+-1.1102230246252e-16)+(0x6fcd17f/0x56114bc)+(0x7421fd/0x37e45a+2.7755575615629e-16)+(0x24d4eecd/0x1613c19+6.6613381477509e-16)+(0x1590688db/0xef01f8d+-1.3877787807814e-17)+(0x31df1eed/0x38037c2+1.1102230246252e-16)+(0x157f4475/0x1361d92)+(0x2d55216f/0x188e0e3+-4.4408920985006e-16)+(0x27d375f/0x25be78+7.7715611723761e-16)+(0xfa0be57/0x20110b0)+(0x3167bae1/0x1a86799+-6.6613381477509e-16)+(0xe8d9af6/0x1163ef3+9.9920072216264e-16)+(0x33154a21/0x1d96467+1.1102230246252e-16)+(0x1a6ac055/0x1267c0d+-2.2204460492503e-16)+(0x87e8fa6/0xffc3c3+8.8817841970013e-16)+(0xd17adfd/0x18504da)+(0x178b441/0x1093672+-6.6613381477509e-16)+(0x23de7ef3/0x1f3076b+-3.885780586188e-16)+(0x4b5f719/0x1888c60+-6.1062266354384e-16)+(0x53e346b7/0x2e0e9a3+1.6653345369377e-16)+(0x1de65674/0x1684261)+(0x6942ea7/0xbed5a3+9.9920072216264e-16)+(0x1983b3/0xeb81da+-6.8001160258291e-16)+(0x506682e/0x11d2923+-1.1102230246252e-16)+(0x1c385a29/0x159e7de+4.4408920985006e-16)+(0xc0bf85df/0xdf9d406+1.1102230246252e-16)+(0x147818ee/0x1372519+2.2204460492503e-16)+(0x391e6a/0x1f3cd1b+-4.1633363423443e-16)+(0x391c096/0x31cdf3+8.8817841970013e-16))This probably looks very secure, but remember: clients can also modify scripts:
KEYCODE = ((0x61e3d0d5/0x5192d6b+2.7755575615629e-17)+(0x1f694311/0x1955b30+-1.1102230246252e-16)+(0x16c0824/0x10b4595)+(0x6a60bf/0x6ff17e+6.6613381477509e-16)+(0x380f46a/0x21b7965+1.1102230246252e-16)+(0x1c50fa7c/0x2d1fded+-9.7144514654701e-17)+(0x114e7e9c/0xbc7a13+4.4408920985006e-16)+(0x1927996/0x1dc8a73+1.1102230246252e-16)+(0x10e9a2c9/0x1b385ba+-5.5511151231258e-16)+(0x11c04e45/0x1bbe5a6+4.1633363423443e-16)+(0x6d89129/0x188efea+5.5511151231258e-16)+(0x289c9e08/0x2b2ac3d+-1.5959455978987e-16)+(0x21401c1/0x56850a+2.7755575615629e-16)+(0x1c7febb5/0x1681ab4+-2.2204460492503e-16)+(0xfed92e/0x17e6bb+-8.8817841970013e-16)+(0x2d1a1d7b/0x2e0f894+2.2204460492503e-16)+(0x7fa3687/0x1667514+-3.3306690738755e-16)+(0x75867d1/0xc44d2d+8.8817841970013e-16)+(0x1d394686/0x1343e31+2.2204460492503e-16)+(0x2d16a30f/0x1402dbfa)+(0xe99a123d/0x8c36f46)+(0x2afc565b/0x30ef60e)+(0x220ec4eb/0x12bba2b+9.1593399531575e-16)+(0x1441e017/0x1611f09+-6.6613381477509e-16)+(0x368d4fcb/0x1fe2618+4.9960036108132e-16)+(0x4d666d3/0x1ea4914+-1.1102230246252e-16)+(0x27717f27/0x1938233+-2.0816681711722e-17)+(0x69f2384/0x3df46cd+1.1102230246252e-16)+(0x1719ad60/0x1e8c151+5.5511151231258e-17)+(0xa420faa/0x17697d7+-7.3378803033819e-16)+(0x38144896/0x289aca1+-1.6653345369377e-16)+(0x54b99a08/0x2d6ee39+2.2204460492503e-16)+(0xfe1b5af/0x351391f)+(0x42849bd6/0x2499517+1.8041124150159e-16)+(0x239166b6/0x209c123+-2.2204460492503e-16)+(0xdd31a45/0x135f00e+2.2204460492503e-16)+(0x2306c475/0x1d6050a+9.7144514654701e-17)+(0x30f26a23/0x260c4f0+-1.1102230246252e-16)+(0x134fad1d/0x2c28044+-2.2204460492503e-16)+(0x27088b5b/0x32d2eb1+-5.5511151231258e-17)+(0xf690566/0xf2128d+-7.7715611723761e-16)+(0x6e85671/0x1fc82d5+-3.3306690738755e-16)+(0xe8b82bd/0xd9c032+-3.7470027081099e-16)+(0x7166f56/0x23c6253+-3.6082248300318e-16)+(0x140b07c/0xadfc3f+4.4408920985006e-16)+(0x174c18ef/0x16d1670+-4.9960036108132e-16)+(0x1a735971/0x1010ea5+-4.9960036108132e-16)+(0x2185f975/0x410c927+-5.5511151231258e-17)+(0x29a2e94b/0x3f3a200+1.1102230246252e-16)+(0x10d3c919/0x1bf439c+1.1102230246252e-16)+(0x36d21f18/0x214764f+-3.885780586188e-16)+(0x1e1dda01/0x28e4fb5+-2.2204460492503e-16)+(0x4ae8b675/0x2870ffa)+(0x405a5c1f/0x2306cdc+2.2204460492503e-16)+(0x30113425/0x2b36963+-1.1102230246252e-16)+(0xc612a9c/0x12ae5ab+-3.3306690738755e-16)+(0x5cf98f0e/0x347d769)+(0xd34634f/0x1ae1810)+(0x107b30d9/0x26e34e4+-1.1102230246252e-16)+(0x6fcd17f/0x56114bc)+(0x7421fd/0x37e45a+2.7755575615629e-16)+(0x24d4eecd/0x1613c19+6.6613381477509e-16)+(0x1590688db/0xef01f8d+-1.3877787807814e-17)+(0x31df1eed/0x38037c2+1.1102230246252e-16)+(0x157f4475/0x1361d92)+(0x2d55216f/0x188e0e3+-4.4408920985006e-16)+(0x27d375f/0x25be78+7.7715611723761e-16)+(0xfa0be57/0x20110b0)+(0x3167bae1/0x1a86799+-6.6613381477509e-16)+(0xe8d9af6/0x1163ef3+9.9920072216264e-16)+(0x33154a21/0x1d96467+1.1102230246252e-16)+(0x1a6ac055/0x1267c0d+-2.2204460492503e-16)+(0x87e8fa6/0xffc3c3+8.8817841970013e-16)+(0xd17adfd/0x18504da)+(0x178b441/0x1093672+-6.6613381477509e-16)+(0x23de7ef3/0x1f3076b+-3.885780586188e-16)+(0x4b5f719/0x1888c60+-6.1062266354384e-16)+(0x53e346b7/0x2e0e9a3+1.6653345369377e-16)+(0x1de65674/0x1684261)+(0x6942ea7/0xbed5a3+9.9920072216264e-16)+(0x1983b3/0xeb81da+-6.8001160258291e-16)+(0x506682e/0x11d2923+-1.1102230246252e-16)+(0x1c385a29/0x159e7de+4.4408920985006e-16)+(0xc0bf85df/0xdf9d406+1.1102230246252e-16)+(0x147818ee/0x1372519+2.2204460492503e-16)+(0x391e6a/0x1f3cd1b+-4.1633363423443e-16)+(0x391c096/0x31cdf3+8.8817841970013e-16))print("KEYCODE="..tostring(KEYCODE))KEYCODE=1234.0The Secure Way
Section titled “The Secure Way”The most secure way of doing this is with a cryptographic hash (like the ones listed here under Hash), which is a cryptologically secure (theoretically) one-way method of encrypting strings.
For example:
-- Blake2B in 64 bytes, encoded to hexKEYCODE_BLAKE2B_HEX = "da77bd2a1d857d88b31de27536b81df7f005027d4f847667df13a0569b6048e0454ce9480827789547cc174060c4f388866ebb0209929b0de414cc9ac571c421"-- ...
-- Check hash of the entered string against our known hashif blake2b.hash({string.byte(boundObject["Text"].text,1,-1)}, 64) == KEYCODE_BLAKE2B_HEX then -- Open doorendRule 1. Scope
Section titled “Rule 1. Scope”Other scripts can call/access your script’s global functions/variables. If you have some code that you want to keep to
your own script, you should use the local keyword. For example:
-- This function can be run by external scripts, which can cause a player to cheat and give himself money-- if they are able to get a reference to your scriptfunction IncrementMoneys(amount) print("Incrementing money to this player!") ...end-- This function can NOT be run by external scripts, because external scripts can only run Global functions.local function SaferIncrementMoneys(amount) print("Incrementing money to this player (a bit more safely)!") ...endSo the rule of the thumb is give your code the least scope possible they need to work.
More to come
Section titled “More to come”Other rules of thumb coming soon!