fix(inbound-api): resolve InboundAPI-001/003/005 — concurrent handler cache, constant-time API key compare, script trust-model enforcement
This commit is contained in:
@@ -132,4 +132,111 @@ public class InboundScriptExecutorTests
|
||||
Assert.True(result.Success);
|
||||
Assert.Contains("ScadaLink", result.ResultJson!);
|
||||
}
|
||||
|
||||
// --- InboundAPI-001: concurrent lazy-compile must not corrupt the handler cache ---
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentLazyCompile_SameMethod_DoesNotCorruptCache()
|
||||
{
|
||||
// Many concurrent first-callers of an uncompiled method race the lazy-compile
|
||||
// path. With an unsynchronized Dictionary this can throw or return a torn/null
|
||||
// handler; all calls must succeed and produce the same result.
|
||||
var method = new ApiMethod("concurrent", "return 7;") { Id = 1, TimeoutSeconds = 10 };
|
||||
|
||||
var tasks = Enumerable.Range(0, 64).Select(_ => Task.Run(() =>
|
||||
_executor.ExecuteAsync(
|
||||
method,
|
||||
new Dictionary<string, object?>(),
|
||||
_route,
|
||||
TimeSpan.FromSeconds(10)))).ToArray();
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
Assert.All(results, r =>
|
||||
{
|
||||
Assert.True(r.Success, r.ErrorMessage);
|
||||
Assert.Equal("7", r.ResultJson);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentRegisterAndExecute_DoesNotThrow()
|
||||
{
|
||||
// RegisterHandler/RemoveHandler racing ExecuteAsync must not crash the process
|
||||
// with an InvalidOperationException from concurrent Dictionary mutation.
|
||||
var method = new ApiMethod("racy", "return 1;") { Id = 1, TimeoutSeconds = 10 };
|
||||
|
||||
var writers = Enumerable.Range(0, 32).Select(i => Task.Run(() =>
|
||||
{
|
||||
for (var n = 0; n < 50; n++)
|
||||
{
|
||||
_executor.RegisterHandler("racy", async ctx => { await Task.CompletedTask; return i; });
|
||||
_executor.RemoveHandler("racy");
|
||||
}
|
||||
}));
|
||||
|
||||
var readers = Enumerable.Range(0, 32).Select(_ => Task.Run(async () =>
|
||||
{
|
||||
for (var n = 0; n < 50; n++)
|
||||
{
|
||||
await _executor.ExecuteAsync(
|
||||
method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
|
||||
}
|
||||
}));
|
||||
|
||||
// Should complete without an unhandled concurrency exception.
|
||||
await Task.WhenAll(writers.Concat(readers));
|
||||
}
|
||||
|
||||
// --- InboundAPI-005: compiled scripts must not bypass the script trust model ---
|
||||
|
||||
[Theory]
|
||||
[InlineData("System.IO.File.Delete(\"/tmp/x\"); return null;")]
|
||||
[InlineData("System.Diagnostics.Process.Start(\"/bin/sh\"); return null;")]
|
||||
[InlineData("var t = System.Reflection.Assembly.GetExecutingAssembly(); return null;")]
|
||||
[InlineData("new System.Threading.Thread(() => {}).Start(); return null;")]
|
||||
[InlineData("var s = new System.Net.Sockets.Socket(System.Net.Sockets.AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); return null;")]
|
||||
public void CompileAndRegister_ForbiddenApi_RejectsScript(string script)
|
||||
{
|
||||
var method = new ApiMethod("forbidden", script) { Id = 1, TimeoutSeconds = 10 };
|
||||
|
||||
var registered = _executor.CompileAndRegister(method);
|
||||
|
||||
Assert.False(registered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ForbiddenApiScript_DoesNotRunAndReturnsFailure()
|
||||
{
|
||||
// A fully-qualified forbidden API call must be rejected at compile/register time
|
||||
// so the script never executes.
|
||||
var marker = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(), $"scadalink-pwned-{Guid.NewGuid():N}");
|
||||
System.IO.File.Delete(marker);
|
||||
|
||||
var method = new ApiMethod("evil", $"System.IO.File.WriteAllText(@\"{marker}\", \"x\"); return 1;")
|
||||
{
|
||||
Id = 1,
|
||||
TimeoutSeconds = 10
|
||||
};
|
||||
|
||||
var result = await _executor.ExecuteAsync(
|
||||
method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(System.IO.File.Exists(marker));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompileAndRegister_PermittedScript_StillRegisters()
|
||||
{
|
||||
// The trust-model check must not reject legitimate scripts.
|
||||
var method = new ApiMethod("ok", "var list = new List<int> { 1, 2, 3 }; return list.Sum();")
|
||||
{
|
||||
Id = 1,
|
||||
TimeoutSeconds = 10
|
||||
};
|
||||
|
||||
Assert.True(_executor.CompileAndRegister(method));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user