4d1787f484
* fix: harden agentctl bind, git clone args, and powershell sound escaping Three LOW-severity defense-in-depth hardening fixes with PoC + regression tests: - notifications: escapePowerShell now neutralizes backtick and `$` in addition to `"`, defusing `$(...)` sub-expression and `$var` injection through an operator-supplied SoundFile path interpolated into the powershell `-c` script. - office/skills: git clone for git-sourced skills now passes a `--` end-of-options separator before the source locator (matching configloader), so a `-`-prefixed locator can never be parsed as a git flag. - agentctl: new Config.ListenHost() binds the control server and per-instance listeners to loopback only when no AuthToken is configured (auth disabled), so the unauthenticated command/shell/process routes are never reachable beyond localhost. The normal nonce-handshake flow (incl. Docker) always has a token and keeps binding all interfaces. * test: make PoC tests flip and drop indexOf coupling Address review feedback on the security-hardening PoCs: - system_provider PoC now demonstrates the PowerShell sub-expression injection against a legacyEscapePowerShell copy of the unfixed function and asserts the current escaper defuses it, so the PoC genuinely flips instead of passing trivially against fixed code (a substring match on `$(` matched the escaped form too). - materialization PoC + regression test use stdlib slices.Index instead of a custom indexOf defined in the PoC file, removing the coupling where deleting the PoC file would break the regression test. --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
40 lines
1.6 KiB
Go
40 lines
1.6 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// TestBearerTokenAuth_EmptyToken_ExposesCommandRoutes_PoC demonstrates the
|
|
// LOW-severity defense-in-depth gap: when AuthToken is empty the bearer-token
|
|
// middleware becomes a pass-through, so a request with NO Authorization header
|
|
// reaches command-execution routes such as /api/v1/processes/start (which runs
|
|
// `sh -lc <command>`), git, and /api/v1/shell/*.
|
|
//
|
|
// This is by design for standalone loopback mode (no nonce → no token), but is
|
|
// a real risk if the listener is ever bound beyond loopback. The fix does NOT
|
|
// change this middleware behavior (that would need a product decision); it
|
|
// enforces loopback-only binding when no token is configured, so this
|
|
// pass-through is only ever reachable from localhost. See the ListenHost guard
|
|
// (config package) and the open question noted in the task report.
|
|
func TestBearerTokenAuth_EmptyToken_ExposesCommandRoutes_PoC(t *testing.T) {
|
|
r := gin.New()
|
|
r.Use(bearerTokenAuth("" /* no token → auth disabled */))
|
|
r.POST("/api/v1/processes/start", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "command executed"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("POST", "/api/v1/processes/start", nil)
|
|
// Note: no Authorization header at all.
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("PoC expected unauthenticated request to reach protected command route (200), got %d", w.Code)
|
|
}
|
|
t.Log("PoC: with empty AuthToken, an unauthenticated POST reaches /api/v1/processes/start")
|
|
}
|