13. 01. 2025 William Calliari Development, Icinga Web 2, PHP

Plugin Systems and Capabilities

On the 36th Chaos Communication Congresses back before Covid forced a three year break, I attended a talk from the German tech-blogger Fefe. There he talked about the “nützlich-unbedenklich Spektrum” or in English, the useful – harmless spectrum. He argued that all software lies on that spectrum to some degree. Of course one could argue that it’s actually a triangle, where effort can optimize both usefulness and harmlessness, but for a given complexity budget I think it’s a good enough approximation to the real world to be useful for our analysis. The more capabilities the user has, to impact the programs behavior, the more chances there are for security vulnerabilities. In this post I want to sort three specific plugin-systems, as representatives of a specific approach, on that spectrum and discuss their advantages and associated risk potential.

This blog-post is not meant to be a conclusive enumeration of all possible plugin-systems with all their advantages and disadvantages, but is meant to illustrate the various considerations when planning a plugin-system for your software.

In particular I will be talking about the following the plugin-systems:

  1. The icingaweb2 plugin-system
  2. The current implementation of the icinga2-notifications plugin-system
  3. The plugin-system of the open-source video game veloren.

The icingaweb2 plugin-system

The icingaweb2 plugin-system is the one with the most capabilities I’ll discuss in this post. It works by loading PHP source-code files from the plugin directory and executing them. It’s similar to the GLPI and WordPress, but since I’m most familiar with icingaweb2, I’ll stick with that. This plugin-system for icingaweb2 comes in three flavors:

First of all there is the configuration.php file. This file is used to register all the necessary permissions and restriction the plugin needs, as well as the side-panel entries and submenus to the module configuration. The configuration part mostly just provides some extra data to icingaweb2 that it already knows how to handle, with some checks to only load certain data when multiple plugins are loaded together.

Secondly there are the hooks. Those get registered when the plugin is loaded in the run.php file. They are attached to a hook name and provide a php class that implements a specific interface. When a specific hook is triggered, the application will then look up all classes registered to that hook name and then loops over the hooks to call them, triggering their behavior.

Lastly, there the “Controller” that allow for custom endpoints and “CliCommands” to extend the capabilities of icingacli. These classes are loaded from the directories application/controllers and application/clicommands respectively. Here, the behavior is directly injected into the routing/cli sub-command resolution of icingaweb2, with no additional code required. They can contain any PHP code and handle a lot of logic through reflection and the respective class and method names.

While those flavors are very differently in what they provide, they actually share one crucial commonality: They all run in the same process with the same global access to all resources of the application. This is great, as it allows the plugin author easy access to all the needed information in a straightforward way. There is no need to consider every aspect that the plugin might want to access data or state. Furthermore it allows for an easy way to share dependencies, as they are already loaded in the application. This means that not every plugin has to ship a way to handle database connections or HTTP routing, but it’s all already provided by the application context.

This pattern is widely used in the PHP world and has certainly helped the huge success of WordPress and their Plugin ecosystem. It’s extremely useful to easily build vastly extensible software, allowing plugin developers to customize the experience of the user in a very granular way. But, as we all know, with great power comes great responsibility.

Since the plugins share the same process and application context, they need to be reviewed very carefully. Any security vulnerability in a plugin has the potential to give an attacker full access to the application, no matter how little of the context the application the plugin actually needs. Furthermore a harmless oversight in one plugin combined with a small bug in another can cascade into a bigger security issue, if an attacker manages to chain them together in a non-trivial way.

I personally really dislike this approach for several reasons: First of all, all the permissions and capabilities of the plugin are implicit. Whatever the code does is the canonical behavior of the plugin and there are not further checks in place to prevent misbehaving code to access resources outside its control. Furthermore, I believe that as much behavior as possible should be immutable. The permissions defined should be static in a configuration file instead of being defined in the code. This would reduce the surface area where behavior can be injected, making it easier to audit the code. Lastly, my functional programming brain really dislikes that hooks can directly mutate data that is passed in. I know that it’s not that big of an issue in the grand scheme of things, but I personally like to think of hooks as being behavior outside of the application that should not have any impact on the state if the code doesn’t explicitly uses the returned data.

The icinga2-notifications plugin-system

The icinga2-notifications plugin-system is much smaller in scope and therefore opted for a different approach. Instead of running the plugins in the same application, each plugin runs in its own process, communication with the main application via a simple JSON-RPC interface over stdin and stdout.

Here to load the necessary context to run the plugin, the plugin needs to implement two JSON-RPC methods: The GetInfo method is called to retrieve the necessary schema for the application configuration. The application then makes sure the user can configure the plugin correctly and provide the requested data. If the configuration can be provided, the SetConfig rpc call is made to initialize the plugin with the configuration.

This approach is really elegant in my opinion. It gives a clear point to inspect the data the plugin is actually allowed to use while keeping the state of the plugin as isolated as possible. This also reduces the information exposed to the plugin to the minimum required, reducing the impact of a possible security vulnerability.

After the plugin is initialized, the application can call the SendNotification method for each notification that needs to be sent, passing in the necessary data of the notification as the method parameters as defined in the JSON-RPC standard. The plugin can then autonomously handle the request and must then answer with the result of the action, providing either a result with an arbitrary json value or an error with an error message.

While this approach limits the plugin authors to only implement a very specific part of the user-experience, it also provides a much clearer interface to the user a a very strict separation of concerns and permissions through the OS process boundary. In theory the plugins capabilities could even be further restricted by using a container runtime like docker or just plain old selinux. Depending on the plugin, removing the possibility to access local files, networks or services could all help reduce the blast radius of potential vulnerabilities.

The plugin-system of veloren

So, until now I focused on two plugin-systems from icinga2, so why the sudden switch to a video game? I actually spent quite a bit of time thinking about how to best implement plugin-systems and how to balance the usefulness of the interface with the security implications and the plugin-system of veloren inspired me to finally write my thoughts down.

I’m writing this blog-post at the 38th Chaos Communication Congress in Hamburg, specifically from the Veloren assembly. I had the huge honor to be able to talk to Christof Petig, one of the core contributors to veloren and all around brilliant mind. Besides hundreds of questions about the networking protocols of the game, I also got to extensively talk about the new plugin-system he developed for veloren.

Christof actually did some work for the Bytecode Alliance himself, he had a small involvement in the WIT specification and did the majority of the work on the work for the C++ backend of the canonical ABI. He has lead me through all the code to understanding how the plugins are implemented, loaded and executed.

A veloren plugin consists of a tar archive which must contain at least a plugin.toml file. That archive can then furthermore contain various assets for models, textures and skeletons. All of that are static files with no code attached, so assuming security while loading the files (which is a pretty easy conclusion to come to, as Rust prevents already the majority of memory vulnerabilities) they pose no real danger to the users.

However should the plugin want to expose custom behavior in code, it allows the plugin author to include WebAssembly files in the archive. If the plugin file in the archive root, specifies any wasm components in the modules section, those will be loaded through the wasmtime runtime. The runtime then limits the application to only perform stdio to provide logs to the game. To confirm that the plugin is actually compliant with the hooks, the runtime will also check the wasm module against the WIT (WebAssembly Interface Types) file. Since I am mostly concerned with code execution, in this context I will focus on those wasm files.

As already mentioned, the plugins code in veloren is shipped in WebAssembly, a sandboxed execution environment with a formal prove of its soundness and memory-safety. This allows the plugin to run arbitrary code in the same process, while maintaining performance and security.

One interesting decision that was made in the veloren plugin-system is that the plugins have absolutely no access to any operating resources outside the stdio. This means that even a malicious plugin could never access the players local files or network, crucial for ensuring the security of a plugin for a game where servers can ship their own plugins to run on the users machines. The only thing the plugin can do, is operations and computations on the data passed in by the game.

Speaking of which, that’s where WIT and the canonical ABI come back into play. The contract between the plugin and the game is defined in an WIT file. From that the code for both the server and plugin side are generated, making sure both sides adhere to the same contract. When first loading the plugin, wasmtime will them make sure all the types and method definitions are actually met, as expected by the game. Then it will compile the WASM down to machine code, exposing the plugin functions over the canonical ABI. The code generated on the games side then makes sure that the type-layout and calling-conventions on the Rust side are the same as the ones of the plugin.

After the plugin is loaded, it is basically just more native assembly running in the same process as the game. The overhead to call such functions is fairly minimal and the performance of the plugin is also very close to native code, while maintaining security.

The obvious downside of this approach is that it is much more limited in its capabilities. While the plugins in PHP usually can hijack a lot of the control-flow of the application if they are inclined to do so, this plugin system only allows you to attach to very specific hooks with very narrowly defined interfaces. While I think this is the perfect choice for a video game, where the game needs to run arbitrary code from a remote server, this might not be the best choice for code that is audited an running locally on a server as the overhead of defining all the interfaces and developing the plugins against them might not be worth the security benefits.

Conclusion

Plugin-systems are in a interesting spot, where they both should allow users to extend the software as much as they like, while balancing it with the inherent security risks of running third-party code in your application. There are many tradeoffs to consider, such as security, performance, stability and ease of use for the plugin authors. The decision between those is often not trivial and may require a lot of thought to be put in to balance the different aspects.

I hope this post gave you some insight into some of different approaches to plugin and how they are used in the wild.nto some of different approaches to
plugin and how they are used in the wild.

William Calliari

William Calliari

Author

William Calliari

Leave a Reply

Your email address will not be published. Required fields are marked *

Archive