fix(central-ui): resolve CentralUI-001 — enforce script trust model before sandbox execution
ScriptAnalysisService.RunInSandboxAsync compiled and executed arbitrary user C# in the central host process with no trust-model enforcement — the forbidden-API set was only a Monaco editor diagnostic. A Design-role user could run System.IO/Process/Reflection/network code on the central node. Added a Roslyn semantic gate (EnforceTrustModel) invoked after compilation and before script.RunAsync, and on nested shared scripts in callSharedFunc; a script referencing any forbidden API is rejected before it runs. Reworked FindForbiddenApiUsages: it now resolves every identifier against the semantic model and checks types and members, so a fully-qualified call (System.IO.File.WriteAllText) is caught — the pre-fix check only inspected the leftmost identifier and missed that shape. This is a static semantic gate, not a process sandbox. Adds gate regression tests that fail against the pre-fix code, plus a clean-script test guarding against over-blocking.
This commit is contained in:
@@ -405,4 +405,64 @@ public class ScriptAnalysisServiceTests
|
||||
Assert.Contains("name", resp.Markdown);
|
||||
Assert.Contains("String", resp.Markdown);
|
||||
}
|
||||
|
||||
// ── CentralUI-001: trust-model gate before sandbox execution ──────────
|
||||
|
||||
[Fact]
|
||||
public void Diagnose_FullyQualifiedForbiddenCall_RaisesSCADA002()
|
||||
{
|
||||
// A forbidden API reached by fully-qualified name (no `using`, no bare
|
||||
// type identifier) must still be flagged — the pre-fix semantic check
|
||||
// only inspected the leftmost identifier and missed this shape.
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
"var d = System.IO.Directory.GetCurrentDirectory(); return d;"));
|
||||
Assert.Contains(resp.Markers, m => m.Code == "SCADA002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_FullyQualifiedForbiddenApi_IsBlockedBeforeExecution()
|
||||
{
|
||||
// Regression test for CentralUI-001. RunInSandboxAsync used to execute any
|
||||
// script that compiled, with no trust-model enforcement — so fully-qualified
|
||||
// forbidden API code ran in the central host process. The fix gates execution
|
||||
// on the forbidden-API analysis.
|
||||
var result = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest(
|
||||
"var d = System.IO.Directory.GetCurrentDirectory(); return d;",
|
||||
Parameters: null,
|
||||
TimeoutSeconds: null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(SandboxErrorKind.CompileError, result.ErrorKind);
|
||||
Assert.Contains("trust model", result.Error);
|
||||
Assert.NotNull(result.Markers);
|
||||
Assert.Contains(result.Markers!, m => m.Code is "SCADA001" or "SCADA002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_ForbiddenUsingDirective_IsBlockedBeforeExecution()
|
||||
{
|
||||
var result = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest(
|
||||
"using System.Diagnostics; var p = Process.GetCurrentProcess().Id; return p;",
|
||||
Parameters: null,
|
||||
TimeoutSeconds: null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(SandboxErrorKind.CompileError, result.ErrorKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_CleanScript_StillRuns()
|
||||
{
|
||||
// The gate must not block a script that stays within the allowed surface.
|
||||
var result = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest("return 21 * 2;", Parameters: null, TimeoutSeconds: null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("42", result.ReturnValueJson);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user