test(02-05): cover proxy, scrub and CLI security boundaries
- Reject non-loopback bind/upstream, cap bodies, isolate concurrent sessions and refuse traversal - Scrub JWT, inv_ tokens, cookies, client secrets and OAuth codes out of fixtures - CLI discovery and replay errors exit nonzero without printing secrets Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
157
cmd/summer/parity_contract_test.go
Normal file
157
cmd/summer/parity_contract_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/bonfire"
|
||||
"git.golem15.com/golem15/summercms/tide"
|
||||
)
|
||||
|
||||
func TestParityCommandContract(t *testing.T) {
|
||||
names := commandNames()
|
||||
for _, want := range []string{"parity:record", "parity:proxy", "parity:replay"} {
|
||||
if !containsName(names, want) {
|
||||
t.Fatalf("missing %s in %v", want, names)
|
||||
}
|
||||
}
|
||||
|
||||
spec := filepath.Join("..", "..", "tide", "testdata", "one-route-spec.yaml")
|
||||
outDir := t.TempDir()
|
||||
fixture := filepath.Join(outDir, "sample.yaml")
|
||||
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSJ9.signaturehere123456"
|
||||
const inv = "inv_abcd1234xyz"
|
||||
|
||||
orig := httptest.NewServer(jsonHandler(`{"data":"ok","token":"` + jwt + `"}`))
|
||||
t.Cleanup(orig.Close)
|
||||
changed := httptest.NewServer(jsonHandler(`{"data":"no","token":"` + jwt + `"}`))
|
||||
t.Cleanup(changed.Close)
|
||||
|
||||
varsPath := filepath.Join(t.TempDir(), "vars.yaml")
|
||||
rulesPath := filepath.Join(outDir, "rules.yaml")
|
||||
if err := os.WriteFile(rulesPath, []byte("client: nuxt\nkeep_request_headers: []\nkeep_response_headers:\n - Content-Type\nroutes:\n - method: GET\n path: /sample\n capture:\n - from: response.json\n path: $.token\n as: jwt:alice\n category: jwt\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := runParityCapture("parity:record", "--spec", spec, "--target", orig.URL, "--output", fixture, "--rules", rulesPath, "--vars", varsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("record: %v\n%s", err, out)
|
||||
}
|
||||
raw, err := os.ReadFile(fixture)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(raw)
|
||||
if strings.Contains(text, jwt) || strings.Contains(text, inv) {
|
||||
t.Fatalf("recorded YAML leaked secret:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "{{jwt:alice}}") {
|
||||
t.Fatalf("recorded YAML missing placeholder:\n%s", text)
|
||||
}
|
||||
assertNoSecrets(t, out+"\n"+text, jwt, inv)
|
||||
|
||||
out, err = runParityCapture("parity:replay", "--fixtures", fixture, "--target", orig.URL, "--vars", varsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("replay identical: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
out, err = runParityCapture("parity:replay", "--fixtures", fixture, "--target", changed.URL, "--vars", varsPath)
|
||||
if err == nil {
|
||||
t.Fatal("changed JSON must fail with nonzero exit")
|
||||
}
|
||||
combined := out + "\n" + err.Error()
|
||||
if !strings.Contains(err.Error(), "$.data") {
|
||||
t.Fatalf("mismatch missing $.data: %s", combined)
|
||||
}
|
||||
assertNoSecrets(t, combined, jwt, inv)
|
||||
|
||||
out, err = runParityCapture("parity:record", "--spec", spec, "--target", orig.URL)
|
||||
if err == nil || !strings.Contains(err.Error(), "--output") {
|
||||
t.Fatalf("missing output must fail: %v %s", err, out)
|
||||
}
|
||||
assertNoSecrets(t, out+"\n"+errString(err), jwt, inv)
|
||||
|
||||
out, err = runParityCapture("parity:proxy", "--rules", rulesPath, "--fixtures", outDir, "--upstream", "http://example.com")
|
||||
if err == nil || !strings.Contains(err.Error(), "loopback") {
|
||||
t.Fatalf("proxy non-loopback: %v %s", err, out)
|
||||
}
|
||||
assertNoSecrets(t, out+"\n"+errString(err), jwt, inv)
|
||||
|
||||
missing := filepath.Join(outDir, "missing-var.yaml")
|
||||
if err := os.WriteFile(missing, []byte("version: 1\nname: miss\nsteps:\n - id: a\n request:\n method: GET\n path: /x/{{missing}}\n response:\n status: 200\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err = runParityCapture("parity:replay", "--fixtures", missing, "--target", orig.URL)
|
||||
if err == nil || !strings.Contains(err.Error(), "unresolved") && !strings.Contains(err.Error(), "placeholder") {
|
||||
t.Fatalf("missing placeholder: %v %s", err, out)
|
||||
}
|
||||
|
||||
unclassified := httptest.NewServer(jsonHandler(`{"token":"` + jwt + `"}`))
|
||||
t.Cleanup(unclassified.Close)
|
||||
leakOut := filepath.Join(outDir, "should-not-exist.yaml")
|
||||
varsForReject := filepath.Join(t.TempDir(), "reject.yaml")
|
||||
out, err = runParityCapture("parity:record", "--spec", spec, "--target", unclassified.URL, "--output", leakOut, "--vars", varsForReject)
|
||||
if err == nil {
|
||||
t.Fatal("unclassified jwt must fail record")
|
||||
}
|
||||
assertNoSecrets(t, out+"\n"+err.Error(), jwt, inv)
|
||||
if _, statErr := os.Stat(leakOut); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("unclassified jwt committed fixture: %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func runParityCapture(args ...string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
root, err := bonfire.NewRootIO("summer", toolCommands(), bytes.NewReader(nil), &buf, &buf)
|
||||
if err != nil {
|
||||
return buf.String(), err
|
||||
}
|
||||
root.SetArgs(args)
|
||||
err = root.Execute()
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func assertNoSecrets(t *testing.T, text string, secrets ...string) {
|
||||
t.Helper()
|
||||
for _, secret := range secrets {
|
||||
if secret != "" && strings.Contains(text, secret) {
|
||||
t.Fatalf("secret %q leaked into CLI output:\n%s", secret, text)
|
||||
}
|
||||
}
|
||||
if strings.Contains(text, "auth_token=") && !strings.Contains(text, "{{") {
|
||||
t.Fatalf("auth_token cookie leaked:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func TestParityCommandContractUnknownCredential(t *testing.T) {
|
||||
store, err := tide.OpenStore("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
step := tide.Step{
|
||||
ID: "bad",
|
||||
Request: tide.Request{
|
||||
Method: http.MethodGet,
|
||||
Path: "/x",
|
||||
Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aaa.bbb"},
|
||||
},
|
||||
Response: tide.Response{Status: 200, Body: tide.Body(`{"ok":true}`)},
|
||||
}
|
||||
if err := tide.ScrubStep(store, &step); err == nil {
|
||||
t.Fatal("unknown leftover jwt must fail")
|
||||
} else if strings.Contains(err.Error(), "eyJ") {
|
||||
t.Fatalf("scrub error leaked jwt: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user