Files
f0c9146c4b MUL-7437: fix(api): classify disabled capabilities as non-retryable (#8487)
* fix(plugin): return 403 when API is disabled (MUL-7437)

Co-authored-by: multica-agent <[email protected]>

* fix(api): classify disabled capabilities as non-retryable (MUL-7437)

Co-authored-by: multica-agent <[email protected]>

* fix(api): address disabled capability review feedback (MUL-7437)

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-16 17:27:05 +08:00

261 lines
9.0 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func pluginSurfaceTokenHandler(t *testing.T) *Handler {
t.Helper()
box, err := NewPluginSurfaceTokenBox(bytes.Repeat([]byte{9}, 32))
if err != nil {
t.Fatalf("NewPluginSurfaceTokenBox: %v", err)
}
return &Handler{PluginSurfaceTokens: box}
}
func validSurfaceClaims() pluginSurfaceLaunchClaims {
return pluginSurfaceLaunchClaims{
WorkspaceID: "01900000-0000-7000-8000-000000000001",
InstallationID: "01900000-0000-7000-8000-000000000002",
VersionID: "01900000-0000-7000-8000-000000000003",
SurfaceKey: "panel",
Digest: "sha256",
Challenge: "bridge-proof-must-stay-opaque",
ExpiresAt: time.Now().Add(time.Minute).Unix(),
}
}
func TestPluginSurfaceTokenIsOpaqueAuthenticatedAndExpiring(t *testing.T) {
h := pluginSurfaceTokenHandler(t)
claims := validSurfaceClaims()
token, err := h.mintPluginSurfaceToken(claims)
if err != nil {
t.Fatalf("mintPluginSurfaceToken: %v", err)
}
if strings.Contains(token, claims.Challenge) || strings.Contains(token, claims.InstallationID) {
t.Fatalf("launch token exposed plaintext claims: %q", token)
}
opened, err := h.openPluginSurfaceToken(token)
if err != nil {
t.Fatalf("openPluginSurfaceToken: %v", err)
}
if opened != claims {
t.Fatalf("opened claims = %+v, want %+v", opened, claims)
}
replacement := byte('A')
if token[10] == replacement {
replacement = 'B'
}
tampered := token[:10] + string(replacement) + token[11:]
if _, err := h.openPluginSurfaceToken(tampered); err == nil {
t.Fatal("tampered launch token was accepted")
}
claims.ExpiresAt = time.Now().Add(-time.Second).Unix()
expired, err := h.mintPluginSurfaceToken(claims)
if err != nil {
t.Fatalf("mint expired token: %v", err)
}
if _, err := h.openPluginSurfaceToken(expired); err == nil {
t.Fatal("expired launch token was accepted")
}
}
func TestPluginSurfaceTokenKeyFailsClosed(t *testing.T) {
if _, err := NewPluginSurfaceTokenBox(nil); err == nil {
t.Fatal("empty deployment key enabled surface tokens")
}
}
func TestPluginSurfaceOriginMustBeOnlyAnHTTPOrigin(t *testing.T) {
for _, value := range []string{"", "plugin.example.test", "javascript:alert(1)", "https://plugin.example.test/path", "https://plugin.example.test?x=1"} {
if _, err := parsePluginSurfaceOrigin(value); err == nil {
t.Fatalf("parsePluginSurfaceOrigin(%q) succeeded", value)
}
}
parsed, err := parsePluginSurfaceOrigin("https://plugin.example.test/")
if err != nil || parsed.String() != "https://plugin.example.test" {
t.Fatalf("valid origin = %v, %v", parsed, err)
}
}
func TestPluginSurfaceOriginMustDifferFromEveryAppOrigin(t *testing.T) {
h := &Handler{cfg: Config{
PublicURL: "https://api.example.test",
AppURL: "https://canonical-app.example.test",
AttachmentFrameAncestors: []string{"https://app.example.test"},
}}
for _, value := range []string{"https://api.example.test", "https://canonical-app.example.test", "https://app.example.test"} {
origin, err := parsePluginSurfaceOrigin(value)
if err != nil {
t.Fatal(err)
}
if h.pluginSurfaceOriginIsDedicated(origin) {
t.Fatalf("%s was accepted as a dedicated content origin", value)
}
}
content, _ := parsePluginSurfaceOrigin("https://plugin-content.example.test")
if !h.pluginSurfaceOriginIsDedicated(content) {
t.Fatal("separate content origin was rejected")
}
}
func TestPluginSurfaceLaunchDistinguishesDisabledFromMisconfigured(t *testing.T) {
tests := []struct {
name string
handler func(t *testing.T) *Handler
wantStatus int
wantCode string
}{
{
name: "missing configuration",
handler: func(*testing.T) *Handler { return &Handler{} },
wantStatus: http.StatusForbidden,
wantCode: "plugin_surfaces_not_configured",
},
{
name: "invalid origin",
handler: func(t *testing.T) *Handler {
h := pluginSurfaceTokenHandler(t)
h.cfg.PluginSurfaceOrigin = "not-an-origin"
return h
},
wantStatus: http.StatusInternalServerError,
wantCode: "plugin_surfaces_misconfigured",
},
{
name: "shared app origin",
handler: func(t *testing.T) *Handler {
h := pluginSurfaceTokenHandler(t)
h.cfg.PluginSurfaceOrigin = "https://app.example.test"
h.cfg.AppURL = "https://app.example.test"
return h
},
wantStatus: http.StatusInternalServerError,
wantCode: "plugin_surfaces_misconfigured",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := tt.handler(t)
withPluginsV1Flag(t, h, true)
recorder := httptest.NewRecorder()
h.GetPluginSurfaceLaunch(recorder, pluginHandlerRequest(http.MethodGet, "/plugins/surface", nil, nil))
if recorder.Code != tt.wantStatus {
t.Fatalf("status=%d body=%s, want %d", recorder.Code, recorder.Body.String(), tt.wantStatus)
}
var body map[string]string
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatalf("decode response: %v", err)
}
if body["code"] != tt.wantCode {
t.Fatalf("code=%q, want %q", body["code"], tt.wantCode)
}
})
}
}
func TestServePluginSurfaceRejectsConfiguredAppOriginBeforeOpeningToken(t *testing.T) {
h := pluginSurfaceTokenHandler(t)
h.cfg.AppURL = "https://app.example.test"
h.cfg.PluginSurfaceOrigin = "https://app.example.test"
token, err := h.mintPluginSurfaceToken(validSurfaceClaims())
if err != nil {
t.Fatalf("mintPluginSurfaceToken: %v", err)
}
request := pluginHandlerRequest(http.MethodGet, "/plugin-surfaces/"+token, nil, map[string]string{"token": token})
request.Host = "app.example.test"
recorder := httptest.NewRecorder()
h.ServePluginSurface(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Fatalf("shared app/content origin: status=%d, want %d", recorder.Code, http.StatusNotFound)
}
}
func TestPluginSurfaceHostExposesOnlySurfaceDocuments(t *testing.T) {
h := &Handler{cfg: Config{PluginSurfaceOrigin: "https://plugin-content.example.test"}}
called := false
boundary := h.PluginSurfaceHostBoundary(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
w.WriteHeader(http.StatusNoContent)
}))
blocked := httptest.NewRequest(http.MethodGet, "https://plugin-content.example.test/api/config", nil)
blocked.Host = "plugin-content.example.test"
blockedRecorder := httptest.NewRecorder()
boundary.ServeHTTP(blockedRecorder, blocked)
if blockedRecorder.Code != http.StatusNotFound || called {
t.Fatalf("content host API request: status=%d called=%v", blockedRecorder.Code, called)
}
allowed := httptest.NewRequest(http.MethodGet, "https://plugin-content.example.test/plugin-surfaces/token", nil)
allowed.Host = "plugin-content.example.test"
allowedRecorder := httptest.NewRecorder()
boundary.ServeHTTP(allowedRecorder, allowed)
if allowedRecorder.Code != http.StatusNoContent || !called {
t.Fatalf("content document request: status=%d called=%v", allowedRecorder.Code, called)
}
}
func TestPluginSurfaceCSPComesOnlyFromGrantedNetScopes(t *testing.T) {
csp := pluginSurfaceCSP([]string{"issues:read", "net:api.example.test", "storage:user", "net:evil.test; script-src *"})
for _, directive := range []string{
"default-src 'none'",
"script-src 'unsafe-inline'",
"connect-src https://api.example.test",
"frame-src 'none'",
"form-action 'none'",
} {
if !strings.Contains(csp, directive) {
t.Fatalf("CSP missing %q: %s", directive, csp)
}
}
if strings.Contains(csp, "issues:read") || strings.Contains(csp, "connect-src https:;") {
t.Fatalf("CSP widened beyond net grants: %s", csp)
}
if strings.Contains(csp, "evil.test") || strings.Contains(csp, "script-src *") {
t.Fatalf("invalid stored net scope escaped into CSP: %s", csp)
}
if noNetwork := pluginSurfaceCSP([]string{"issues:read"}); !strings.Contains(noNetwork, "connect-src 'none'") {
t.Fatalf("no-network CSP = %s", noNetwork)
}
}
func TestPluginSurfaceDocumentHidesBootstrapProofBeforePluginCode(t *testing.T) {
code := `const html = "</script><script>alert(1)</script>";`
document := buildPluginSurfaceDocument(code, "bridge-proof")
if strings.Contains(document, "alert(1)") || strings.Contains(document, `"</script>`) {
t.Fatal("plugin source escaped its inert base64 element")
}
for _, required := range []string{
"new MessageChannel()",
pluginSurfacePortGlobal,
pluginSurfaceConnectMessage,
"challenge = \"\"",
`window.addEventListener("error", reportSurfaceError)`,
`window.addEventListener("unhandledrejection", reportSurfaceError)`,
"codeElement.remove()",
"bootstrapElement.remove()",
} {
if !strings.Contains(document, required) {
t.Fatalf("surface document missing %q", required)
}
}
if strings.Index(document, "bootstrapElement.remove()") > strings.Index(document, "document.body.appendChild(plugin)") {
t.Fatal("plugin code runs before its launch proof is removed from the DOM")
}
if strings.Index(document, `window.addEventListener("error", reportSurfaceError)`) > strings.Index(document, "document.body.appendChild(plugin)") {
t.Fatal("plugin code runs before browser error reporting is armed")
}
}