Rust Plugin Security: Permissions, Input Validation and Backdoor Review for Oxide and Carbon
Secure a Rust plugin by checking a permission in every chat and console command, validating all input (amounts, item names, negative numbers), never executing player-supplied strings as console commands, rate limiting repeatable actions and re-checking server-side everything a CUI button sends. When installing third-party plugins, read the source for web requests, hidden permission grants and console command execution before loading it.
6 min readUpdated Oct 26, 2026
Rust plugin security comes down to one rule: every command, console command and CUI button is input from an untrusted client, so check permission, validate every value and act only on server-side state. Oxide and Carbon do not sandbox plugins, so the same discipline applies when you install code written by someone else.
Treat every entry point as untrusted
Three things let a player reach your code: chat commands, console commands and hooks driven by player actions. CUI buttons and F1 console input are the same entry point, because a button only runs a console command that the client sends.
A player who can see no button can still type the command by hand. Hiding UI is cosmetic. The check must be in the handler.
Check permissions in every command
Register one permission per action, with the lowercase class name as prefix, and check it at the top of every handler.
using System.Linq;
using UnityEngine;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("Safe Give", "RustPluginGenerator", "1.0.0")]
[Description("Gives an item to the caller with permission, amount limits and a cooldown")]
public class SafeGive : RustPlugin
{
private const string PermUse = "safegive.use";
private PluginConfig config;
private readonly Dictionary<ulong, float> lastUse = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Maximum amount per use")]
public int MaxAmount = 100;
[JsonProperty("Cooldown seconds")]
public float Cooldown = 5f;
[JsonProperty("Allowed item shortnames")]
public List<string> Allowed = new List<string> { "wood", "stones", "metal.fragments" };
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPerm"] = "You do not have permission to use this.",
["Usage"] = "Usage: /sgive <shortname> <amount>",
["BadItem"] = "That item is not allowed.",
["BadAmount"] = "Amount must be between 1 and {0}.",
["Wait"] = "Wait {0} seconds before using this again.",
["Given"] = "You received {0}x {1}."
}, this);
}
private string Msg(string key, string userId = null, params object[] args) =>
string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
permission.RegisterPermission(PermUse, this);
}
[ChatCommand("sgive")]
private void CmdGive(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPerm", player.UserIDString));
return;
}
if (args == null || args.Length < 2)
{
player.ChatMessage(Msg("Usage", player.UserIDString));
return;
}
string shortname = args[0].ToLowerInvariant();
if (!config.Allowed.Contains(shortname))
{
player.ChatMessage(Msg("BadItem", player.UserIDString));
return;
}
int amount;
if (!int.TryParse(args[1], out amount) || amount < 1 || amount > config.MaxAmount)
{
player.ChatMessage(Msg("BadAmount", player.UserIDString, config.MaxAmount));
return;
}
ulong id = player.userID.Get();
float now = UnityEngine.Time.realtimeSinceStartup;
float last;
if (lastUse.TryGetValue(id, out last) && now - last < config.Cooldown)
{
player.ChatMessage(Msg("Wait", player.UserIDString, Math.Ceiling(config.Cooldown - (now - last))));
return;
}
Item item = ItemManager.CreateByName(shortname, amount);
if (item == null) return;
lastUse[id] = now;
player.GiveItem(item);
player.ChatMessage(Msg("Given", player.UserIDString, amount, shortname));
}
private void Unload()
{
lastUse.Clear();
}
}
}This pattern covers the main points: permission first, argument count checked, item name restricted to a whitelist, amount bounded on both sides and a per-player cooldown.
Console commands: arg.IsAdmin and arg.Player()
Console commands can come from a player in F1, from the server console or from RCON. Decide who may call yours and enforce it.
arg.Player()is null for the server console and RCON. Check it before use.arg.IsAdminis true for server-level callers and for players with admin status. Use it for commands that must be admin-only.- For plugin features, prefer a permission check with
arg.Player()over blanket admin status.
[ConsoleCommand("safegive.reset")]
private void CcmdReset(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin)
{
arg.ReplyWith("You are not allowed to run this command.");
return;
}
lastUse.Clear();
arg.ReplyWith("Cooldowns cleared.");
}See chat commands vs console commands for the differences between the two entry points, and Oxide permissions and groups for how to grant access.
Validate all input
Never assume a number is sensible because your UI offered sensible numbers. Validate:
- Parsing: use
int.TryParse, notint.Parse, so garbage does not throw. - Range: reject zero and negative values. A negative amount in a sell or transfer command can turn a payment into a withdrawal reversed in the player's favor.
- Upper bound: cap amounts so a player cannot create a stack larger than the item allows or lag the server with huge spawns.
- Item names:
ItemManager.CreateByNamereturns null for unknown shortnames, so check the result, and for player-facing commands use a whitelist. - Targets: resolve players with
BasePlayer.FindAwakeOrSleepingand handle null; do not assume the name matched. - Args array:
argscan be shorter than expected. Checkargs.Lengthbefore indexing, or you get the exception described in IndexOutOfRangeException in commands.
Never run player-supplied strings as commands
Do not build a console command from text a player typed and run it, for example forwarding args into ConsoleSystem.Run or player.SendConsoleCommand. That is arbitrary command execution: the player can pass ownerid or oxide.grant instead of your intended argument.
If a plugin runs commands, such as a kit or shop command from the config, only run strings the server owner wrote in the config. Substitute a validated value like the player's ID into a fixed template, and never insert free text. Treat the config as admin-controlled code and keep its write access limited.
Rate limiting
Anything that gives items, teleports, spawns entities or saves data needs a cooldown or request limit. Store the last-use time per player, as in the example, using UnityEngine.Time.realtimeSinceStartup. Remove entries when a player disconnects in OnPlayerDisconnected or clear the dictionary on unload so it does not grow forever.
For CUI buttons, rate limiting matters even more, since a client can send the underlying command as fast as it likes.
Never trust CUI button commands
A CUI button just says "when clicked, run this console command". The client can run it anytime, with any arguments, even if the panel is closed. So:
- Check permission in the command handler, not when you draw the panel.
- Re-validate every argument. If the button carries an item name or price, look the real value up on the server instead of trusting the one sent.
- Prefer sending a short key or index and resolving it to the real data server-side.
- Confirm the state still holds: the player is alive, the offer still exists, the player still has the money.
See CUI basics for building panels and commands.
Safe data handling
- Store data only with
Interface.Oxide.DataFileSystemand config withConfig, as covered in data files and config files. - Key records by
UserIDStringor the ulong ID, never by display name, which players can change and spoof. - Do not store anything sensitive, such as IP addresses, unless you need it. Data files are plain JSON.
- Treat loaded data as possibly corrupted or hand-edited: null-check, and fall back to a new object. Save in OnServerSave and in
Unload. - Never put secrets in the config of a plugin you share.
Hooks: do not break other plugins
For many hooks such as CanBuild, CanLootEntity and OnEntityTakeDamage, any non-null return value cancels the action, even true. A security check that returns the wrong thing can lock players out or leave a gate open. Return null when you have no opinion, and check each hook page for how its return value is used. See Oxide hooks explained.
Reviewing third-party plugins for backdoors
Plugins run with full server rights and Oxide does not sandbox them. Read the .cs file before installing it, especially from an unofficial source, and again after every update. Search for these patterns:
| Search for | Why it matters |
|---|---|
webrequest, WebRequests, HttpClient, WebClient, Enqueue | Sends data out or downloads instructions. Legitimate for Discord or update checks, but verify the URL and the payload. |
oxide.grant, oxide.usergroup, GrantUserPermission, AddUserGroup | A hidden grant of permissions or admin groups to a specific ID. |
ownerid, moderatorid, ServerUsers.Set | Gives a player server admin status. |
ConsoleSystem.Run, rcon., server.command | Runs arbitrary server commands. Check which strings can reach it. |
Convert.FromBase64String, long encoded strings, XOR loops | Hidden or obfuscated code. Treat as a red flag. |
| Hard-coded 17-digit Steam IDs | Special handling for one account, often a backdoor. |
System.IO, File., Process, Assembly, Reflection | Reaching outside the plugin's own data, config and logs. |
Interface.Oxide.RootPluginManager, GetPlugin, Call( | Controlling or tampering with other plugins. |
Also watch for these:
- A plugin that does not match its description, such as a "clean chat" plugin that needs web access.
- Compiled or obfuscated files, or code split to hide what runs.
- Paid or "free" plugins from unknown uploaders with no history.
After loading, run oxide.show perms, check oxide.show user <id> for unexpected group membership and watch the OnUserPermissionGranted hook if you need an audit trail. Keep admin command logging on so changes leave evidence.
Operational checklist
- Grant staff plugin permissions, not full admin.
- Keep RCON passwords strong and not reused, and restrict the RCON port.
- Limit write access to
oxide/plugins,oxide/configandoxide/data, since anyone who can write there can run code on the server. On Carbon the folders arecarbon/plugins,carbon/configsandcarbon/data. - Test new plugins on a staging server first.
- Read the console after loading, and use debugging Rust plugins when something is off.
Following these habits, your own plugins hold up against a hostile client and your server stays protected from code you did not write.
FAQ
Can a plugin steal data from my Rust server?
Yes. Oxide plugins run with the server's own privileges, so a malicious plugin can read files it can reach, send data over web requests, grant itself permissions or run console commands. Oxide does not sandbox plugins. The only real protection is reading the source before you put it in oxide/plugins.
Is it safe to compile plugins I downloaded as a .cs file?
A .cs file is plain source code, so you can read it before loading. That is an advantage over closed binaries, but only if you actually review it. Compilation itself does not make it safe: once loaded, the plugin runs with full server rights. Search for webrequests, oxide.grant and ConsoleSystem.Run first.
Can players call my console commands from their F1 console?
Yes. Any connected client can type a registered console command in F1, and CUI button commands are the same thing. arg.Player() is the sender, so a command meant for staff must verify permission or arg.IsAdmin itself. Hiding the button in the UI is not protection.
Why does arg.Player() return null and is that a risk?
arg.Player() is null when the command comes from the server console or RCON. It is a risk if you dereference it without a check, which throws an exception. Treat null as a trusted server-side caller only if you intend that, and handle it explicitly rather than by accident.
Should I give moderators full admin to use a plugin?
No. Grant the specific plugin permission, for example with oxide.grant user <id> myplugin.use, or put staff in a group. Full admin status (ownerid or moderatorid) gives access far beyond one plugin. Use the least privilege that lets the person do their job.
More guides
- CovalencePlugin vs RustPlugin: Differences and Which to Use4 min read
- How to Debug a Rust Oxide Plugin: Compile Errors, Exceptions and Slow Hooks10 min read
- How to Give Items to Players in a Rust Plugin (Oxide/Carbon)5 min read
- How to Install Oxide on a Rust Dedicated Server (Windows and Linux)7 min read
Skip the C#: describe your plugin
Open generator