Files
kandev/apps/backend/internal/agentctl/server/api/auth_poc_test.go
T
João Salavisa 4d1787f484 fix: harden agentctl bind, git clone args, and powershell sound escaping (#1811)
* 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>
2026-07-20 11:32:43 +01:00

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")
}