Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

106 lines
4.1 KiB
Go

package mcp
import (
"context"
"os"
"path/filepath"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/indexer"
"github.com/zzet/gortex/internal/query"
)
func newReadGuardServer(t *testing.T, repoRoot string) *Server {
t.Helper()
g := graph.New()
reg := testRegistry()
idx := indexer.New(g, reg, config.IndexConfig{}, zap.NewNop())
idx.SetRootPath(repoRoot)
return NewServer(query.NewEngine(g), g, idx, nil, zap.NewNop(), nil)
}
func readFileTool(t *testing.T, srv *Server, args map[string]any) *mcplib.CallToolResult {
t.Helper()
req := mcplib.CallToolRequest{}
req.Params.Name = "read_file"
req.Params.Arguments = args
res, err := srv.handleReadFile(context.Background(), req)
require.NoError(t, err)
return res
}
func readText(t *testing.T, res *mcplib.CallToolResult) string {
t.Helper()
text, _ := singleTextContent(res)
return text
}
// TestSymlinkGuardRefusesEscape proves the path-traversal guard: read_file
// refuses any path whose REAL location escapes the indexed repository root —
// an in-repo symlink pointing out, or a bare absolute path — while still
// serving in-repo files and in-repo symlinks.
func TestSymlinkGuardRefusesEscape(t *testing.T) {
repoRoot := t.TempDir()
outside := t.TempDir()
secret := filepath.Join(outside, "secret.txt")
require.NoError(t, os.WriteFile(secret, []byte("TOP-SECRET-VALUE"), 0o600))
inside := filepath.Join(repoRoot, "ok.go")
require.NoError(t, os.WriteFile(inside, []byte("package x\n"), 0o644))
escape := filepath.Join(repoRoot, "escape.txt")
require.NoError(t, os.Symlink(secret, escape))
internalLink := filepath.Join(repoRoot, "in-link.go")
require.NoError(t, os.Symlink(inside, internalLink))
srv := newReadGuardServer(t, repoRoot)
// 1. An in-repo symlink escaping the repo is refused; the secret never leaks.
esc := readFileTool(t, srv, map[string]any{"path": escape})
require.True(t, esc.IsError, "a symlink escaping the repo must be refused")
require.NotContains(t, readText(t, esc), "TOP-SECRET-VALUE")
require.Contains(t, readText(t, esc), "outside every indexed repository")
// 2. A bare absolute path outside the repo is refused too.
abs := readFileTool(t, srv, map[string]any{"path": secret})
require.True(t, abs.IsError, "an absolute path outside the repo must be refused")
require.NotContains(t, readText(t, abs), "TOP-SECRET-VALUE")
// 3. A normal in-repo file reads fine (no false positive).
ok := readFileTool(t, srv, map[string]any{"path": inside})
require.False(t, ok.IsError, "an in-repo file must read: %s", readText(t, ok))
// 4. An in-repo symlink to another in-repo file reads fine.
intl := readFileTool(t, srv, map[string]any{"path": internalLink})
require.False(t, intl.IsError, "an in-repo symlink must read: %s", readText(t, intl))
}
// TestConfigRedactAcrossReadTools proves the key-only config redaction is
// applied on the read_file surface: a secret-shaped value is withheld by
// default and served verbatim only with allow_secrets.
func TestConfigRedactAcrossReadTools(t *testing.T) {
repoRoot := t.TempDir()
const secretVal = "ghp_0123456789abcdefghijklmnopqrstuvwxyz"
cfgPath := filepath.Join(repoRoot, "config.yaml")
require.NoError(t, os.WriteFile(cfgPath, []byte("token: "+secretVal+"\n"), 0o644))
srv := newReadGuardServer(t, repoRoot)
// Default: the secret value is withheld; the key survives.
redacted := readFileTool(t, srv, map[string]any{"path": cfgPath})
require.False(t, redacted.IsError, "config read must not error: %s", readText(t, redacted))
require.NotContains(t, readText(t, redacted), secretVal, "secret value must be redacted by default")
require.Contains(t, readText(t, redacted), "token", "the key must survive redaction")
// allow_secrets serves the value verbatim.
raw := readFileTool(t, srv, map[string]any{"path": cfgPath, "allow_secrets": true})
require.False(t, raw.IsError)
require.Contains(t, readText(t, raw), secretVal, "allow_secrets must serve the value verbatim")
}