Fixed a security issue in how AI Studio runs plugins (#1054)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-12 14:54:30 +02:00
1 parent 57dda9ff8b
commit b371b3d54f
6 files changed
+370 -16

No files matched your search

@@ -79,6 +79,8 @@ Each assistant plugin lives in its own directory under the assistants plugin roo
└── plugin.lua
```
A plugin may split its Lua code into several files and load them with `require`. Name the file relative to the plugin directory and without `.lua`, and separate subdirectories with a slash: `require("icon")` loads `icon.lua`, and `require("lib/helpers")` loads `lib/helpers.lua`. A plugin can only load files from its own directory, and `dofile` and `loadfile` are not available.
## Structure
- `ASSISTANT` is the root table. Every assistant requires `Title` and `Description`.
- Form assistants additionally require `SystemPrompt`, `SubmitText`, `AllowProfiles`, and a nested `UI` definition.
@@ -4,6 +4,23 @@ These instructions cover the plugin system in `app/MindWork AI Studio/Tools/Plug
configuration plugins can provide. They add to the `AGENTS.md` in the repository root, which applies here
as well.
## Lua runtime
Every plugin runs in a Lua runtime of its own, created by `PluginFactory.CreateLuaState`, whatever its type
and whoever runs it: loading, starting, checking an import, and the assistant builder.
- A plugin loads the files of its own directory with `require`, through `PluginLoader`, also from
subdirectories: `require("lib/helpers")` loads `lib/helpers.lua`. The dot notation `require("lib.helpers")`
is not supported.
- No other file is within reach. The runtime gets `NoLuaFileSystem` as its file system, `dofile` and
`loadfile` are removed, and code without a directory gets `NoModuleLoader`. With the default file system
of Lua-CSharp, `require` would look for a module it does not find next to the plugin along `package.path`,
relative to the working directory of the app, and `dofile` and `loadfile` would open any path.
- Do not open the `io`, `os`, or `debug` library, and do not give the runtime a file system. Whatever a
plugin needs to reach outside of Lua comes as a host function which checks every access.
- `PluginModuleLoadingTests` checks both sides: what plugins must load, and what they must not reach. Run
it after every change to the runtime, the module loader, or the opened libraries.
## Configuration plugins
Plugins can configure:
@@ -0,0 +1,35 @@
using Lua.IO;
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// The file system of the Lua runtime of plugins, in which no file exists.
/// </summary>
/// <remarks>
/// The Lua runtime reaches files through its file system: dofile and loadfile do, and so does require
/// for every module the module loader does not know, which it then looks for along package.path,
/// relative to the working directory of the app. Plugins are shared between users, and the assistant
/// builder runs code a model wrote, so none of them may reach a file this way.<br/><br/>
/// A plugin still loads the files of its own directory with require: the module loader, see
/// PluginLoader, reads them without this file system.
/// </remarks>
public sealed class NoLuaFileSystem : ILuaFileSystem
{
private const string NO_FILES = "Plugins cannot reach files through the Lua runtime.";
#region Implementation of ILuaFileSystem
public bool IsReadable(string path) => false;
public ValueTask<ILuaStream> Open(string path, LuaFileOpenMode mode, CancellationToken cancellationToken) => throw new NotSupportedException(NO_FILES);
public ValueTask Rename(string oldName, string newName, CancellationToken cancellationToken) => throw new NotSupportedException(NO_FILES);
public ValueTask Remove(string path, CancellationToken cancellationToken) => throw new NotSupportedException(NO_FILES);
public string GetTempFileName() => throw new NotSupportedException(NO_FILES);
public ValueTask<ILuaStream> OpenTempFileStream(CancellationToken cancellationToken) => throw new NotSupportedException(NO_FILES);
#endregion
}
@@ -5,6 +5,7 @@ using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem.Assistants;
using Lua;
using Lua.Platforms;
using Lua.Standard;
namespace AIStudio.Tools.PluginSystem;
@@ -26,6 +27,11 @@ public static partial class PluginFactory
/// </remarks>
private static readonly TimeSpan PLUGIN_TIMEOUT = TimeSpan.FromSeconds(10);
/// <summary>
/// The platform of the Lua runtime of every plugin: the default one, but without any file.
/// </summary>
private static readonly LuaPlatform PLUGIN_PLATFORM = LuaPlatform.Default with { FileSystem = new NoLuaFileSystem() };
/// <summary>
/// A list of all available plugins.
/// </summary>
@@ -419,6 +425,49 @@ public static partial class PluginFactory
return pluginTimeout;
}
/// <summary>
/// Creates the Lua runtime a plugin runs in, with its libraries opened and no code run yet.
/// </summary>
/// <remarks>
/// Every plugin, whatever its type, gets its runtime from here, so all of them see the same
/// libraries and the same limits.<br/><br/>
/// A plugin may split its code into several files and load them with require, also from
/// subdirectories of its own directory, such as require("lib/helpers"). The module loader only
/// finds files inside the plugin's directory.<br/><br/>
/// No other file is within reach: the runtime has no file system, see NoLuaFileSystem, and neither
/// dofile nor loadfile. Code without a directory, such as a plugin a model just wrote, loads no
/// module at all.
/// </remarks>
/// <param name="pluginPath">The directory the plugin is located in, or null when the code has no directory yet.</param>
/// <param name="allowedBaseDirectory">The directory the plugin path must be nested in, see Load.</param>
/// <returns>The runtime for a single plugin.</returns>
internal static LuaState CreateLuaState(string? pluginPath, string? allowedBaseDirectory = null)
{
var state = LuaState.Create(PLUGIN_PLATFORM);
state.ModuleLoader = string.IsNullOrWhiteSpace(pluginPath)
? new NoModuleLoader()
: new PluginLoader(pluginPath, allowedBaseDirectory);
// Add some useful libraries:
state.OpenBasicLibrary();
state.OpenModuleLibrary();
state.OpenStringLibrary();
state.OpenTableLibrary();
state.OpenMathLibrary();
state.OpenBitwiseLibrary();
state.OpenCoroutineLibrary();
//
// Both read a file by its path, relative to the working directory of the app. Without a file
// system they would fail anyway, but with an exception of .NET instead of a Lua error. Such an
// exception leaves Load, instead of becoming the reason why the plugin could not run:
//
state.Environment["dofile"] = LuaValue.Nil;
state.Environment["loadfile"] = LuaValue.Nil;
return state;
}
/// <param name="pluginPath">The directory the plugin is located in, or null when the code has no directory yet.</param>
/// <param name="code">The Lua code of the plugin's main file.</param>
/// <param name="allowedBaseDirectory">
@@ -432,22 +481,7 @@ public static partial class PluginFactory
if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState)
return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}");
var state = LuaState.Create();
if (!string.IsNullOrWhiteSpace(pluginPath))
{
// Add the module loader so that the plugin can load other Lua modules:
state.ModuleLoader = new PluginLoader(pluginPath, allowedBaseDirectory);
}
// Add some useful libraries:
state.OpenBasicLibrary();
state.OpenModuleLibrary();
state.OpenStringLibrary();
state.OpenTableLibrary();
state.OpenMathLibrary();
state.OpenBitwiseLibrary();
state.OpenCoroutineLibrary();
var state = CreateLuaState(pluginPath, allowedBaseDirectory);
try
{
await state.DoStringAsync(code, cancellationToken: cancellationToken);
@@ -35,6 +35,7 @@
- Fixed the log file of AI Studio showing the wrong time for some entries when AI Studio was busy. Each entry now shows when it actually happened.
- Fixed AI Studio pausing for a moment while it opened the local index of a data source, such as a mailbox. Other work, for example applying the configuration of your organization, had to wait until then.
- Fixed a security issue in how AI Studio prepares images for the Visual Briefing assistant. AI Studio now protects all parts of its internal connection in one central place, and an automated test checks that no part is left out.
- Fixed a security issue in how AI Studio runs plugins. A plugin can now only load files from its own folder, and automated tests check this.
- Fixed the transcripts of your audio and video files disappearing when you edited your last message in a chat. You no longer need to attach the files again. Thanks, Bernhard, for reporting this issue.
- Fixed the message you had not sent yet losing its text or the transcripts of your audio and video files when you moved the chat to a workspace.
- Fixed the workspace list no longer showing the chats of your workspaces after you moved a chat to another workspace.