mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 10:33:48 +00:00
Fixed a security issue in how AI Studio runs plugins (#1054)
This commit is contained in:
1 parent
57dda9ff8b
commit
b371b3d54f
6 files changed
+370
-16
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user