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)
|
||||
}
|
||||
}
|
||||
427
tide/proxy_security_test.go
Normal file
427
tide/proxy_security_test.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package tide
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProxySecurity(t *testing.T) {
|
||||
t.Run("rejects non-loopback bind and client-selected upstream", func(t *testing.T) {
|
||||
fixtures := t.TempDir()
|
||||
rules := mustParseRules(t, testRulesYAML())
|
||||
if _, err := NewProxy(ProxyConfig{Listen: "0.0.0.0:8422", Upstream: DefaultUpstream, Fixtures: fixtures, Rules: rules}); err == nil || !strings.Contains(err.Error(), "loopback") {
|
||||
t.Fatalf("non-loopback listen: %v", err)
|
||||
}
|
||||
if _, err := NewProxy(ProxyConfig{Listen: DefaultListen, Upstream: "http://example.com", Fixtures: fixtures, Rules: rules}); err == nil || !strings.Contains(err.Error(), "loopback") {
|
||||
t.Fatalf("non-loopback upstream: %v", err)
|
||||
}
|
||||
if _, err := NewProxy(ProxyConfig{Listen: DefaultListen, Upstream: "https://127.0.0.1:8423", Fixtures: fixtures, Rules: rules}); err == nil || !strings.Contains(err.Error(), "http") {
|
||||
t.Fatalf("https upstream: %v", err)
|
||||
}
|
||||
|
||||
var seenHost []string
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
seenHost = append(seenHost, r.Host)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
proxy := newTestProxy(t, upstream.URL, fixtures, testRulesYAML())
|
||||
srv := httptest.NewServer(proxy.Handler())
|
||||
t.Cleanup(srv.Close)
|
||||
req, err := http.NewRequest(http.MethodGet, srv.URL+"/sample", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Host = "evil.example"
|
||||
req.Header.Set(SessionHeader, "pin")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status %d", resp.StatusCode)
|
||||
}
|
||||
wantHost := strings.TrimPrefix(upstream.URL, "http://")
|
||||
if len(seenHost) != 1 || seenHost[0] != wantHost {
|
||||
t.Fatalf("upstream host %v want %s", seenHost, wantHost)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("request and response size caps leave no fixture", func(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("hello-world"))
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
fixtures := t.TempDir()
|
||||
proxy, err := NewProxy(ProxyConfig{
|
||||
Listen: "127.0.0.1:0",
|
||||
Upstream: upstream.URL,
|
||||
Fixtures: fixtures,
|
||||
Rules: mustParseRules(t, testRulesYAML()),
|
||||
MaxBody: 4,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := httptest.NewServer(proxy.Handler())
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, srv.URL+"/sample", strings.NewReader("12345"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set(SessionHeader, "reqcap")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("request overflow status %d", resp.StatusCode)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fixtures, "nuxt", "reqcap.yaml")); !os.IsNotExist(err) {
|
||||
t.Fatalf("request overflow committed fixture: %v", err)
|
||||
}
|
||||
|
||||
get, err := http.NewRequest(http.MethodGet, srv.URL+"/sample", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
get.Header.Set(SessionHeader, "respcap")
|
||||
gresp, err := http.DefaultClient.Do(get)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gresp.Body.Close()
|
||||
if gresp.StatusCode == http.StatusOK {
|
||||
t.Fatal("response overflow recorded as success")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fixtures, "nuxt", "respcap.yaml")); !os.IsNotExist(err) {
|
||||
t.Fatalf("response overflow committed fixture: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cookie redirect passthrough and concurrent sessions", func(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/go" {
|
||||
w.Header().Set("Set-Cookie", "sid=abc; Path=/")
|
||||
w.Header().Set("Location", "/landed")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
_, _ = w.Write([]byte("redirect-body"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("cookie=" + r.Header.Get("Cookie") + " path=" + r.URL.Path))
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
fixtures := t.TempDir()
|
||||
proxy := newTestProxy(t, upstream.URL, fixtures, testRulesYAML())
|
||||
srv := httptest.NewServer(proxy.Handler())
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
req, err := http.NewRequest(http.MethodGet, srv.URL+"/go", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set(SessionHeader, "redir")
|
||||
req.Header.Set("Cookie", "keep=1")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusFound || resp.Header.Get("Location") != "/landed" {
|
||||
t.Fatalf("redirect %d %s", resp.StatusCode, resp.Header.Get("Location"))
|
||||
}
|
||||
if !strings.Contains(resp.Header.Get("Set-Cookie"), "sid=abc") || string(body) != "redirect-body" {
|
||||
t.Fatalf("cookie/body passthrough cookie=%q body=%q", resp.Header.Get("Set-Cookie"), body)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, 2)
|
||||
for _, name := range []string{"alpha", "beta"} {
|
||||
wg.Add(1)
|
||||
go func(session string) {
|
||||
defer wg.Done()
|
||||
req, err := http.NewRequest(http.MethodGet, srv.URL+"/"+session, nil)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
req.Header.Set(SessionHeader, session)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
errCh <- fmt.Errorf("session %s status %d", session, resp.StatusCode)
|
||||
}
|
||||
}(name)
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := proxy.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
alpha, err := LoadFlow(filepath.Join(fixtures, "nuxt", "alpha.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
beta, err := LoadFlow(filepath.Join(fixtures, "nuxt", "beta.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(alpha.Steps) != 1 || alpha.Steps[0].Request.Path != "/alpha" {
|
||||
t.Fatalf("alpha: %+v", alpha.Steps)
|
||||
}
|
||||
if len(beta.Steps) != 1 || beta.Steps[0].Request.Path != "/beta" {
|
||||
t.Fatalf("beta: %+v", beta.Steps)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path traversal session and sidecar digest", func(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
fixtures := t.TempDir()
|
||||
proxy := newTestProxy(t, upstream.URL, fixtures, testRulesYAML())
|
||||
srv := httptest.NewServer(proxy.Handler())
|
||||
t.Cleanup(srv.Close)
|
||||
req, err := http.NewRequest(http.MethodGet, srv.URL+"/sample", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set(SessionHeader, "../escape")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
t.Fatal("traversal session must fail")
|
||||
}
|
||||
if entries, _ := os.ReadDir(filepath.Dir(fixtures)); len(entries) == 0 {
|
||||
t.Fatal("temp fixtures dir vanished")
|
||||
}
|
||||
unsafe := "version: 1\nname: bin\nsteps:\n - id: a\n request:\n method: GET\n path: /bin\n response:\n body_file: ../secret.bin\n"
|
||||
p := filepath.Join(fixtures, "unsafe.yaml")
|
||||
if err := os.WriteFile(p, []byte(unsafe), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadFlow(p); err == nil {
|
||||
t.Fatal("parent sidecar must fail")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("scrubs credentials across header cookie query json and form", func(t *testing.T) {
|
||||
const (
|
||||
invTok = "inv_abcd1234xyz"
|
||||
secret = "superSecretValue99"
|
||||
code = "oauthCodeValue99"
|
||||
cookieV = "cookieSecretValue99"
|
||||
)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/login":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Set-Cookie", "auth_token="+testJWT)
|
||||
_, _ = w.Write([]byte(`{"token":"` + testJWT + `"}`))
|
||||
case r.URL.Path == "/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
case r.URL.Path == "/authorize":
|
||||
w.Header().Set("Location", "/cb?code="+code)
|
||||
w.WriteHeader(http.StatusFound)
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
}
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
fixtures := t.TempDir()
|
||||
varsPath := filepath.Join(t.TempDir(), "vars.yaml")
|
||||
rules := mustParseRules(t, ""+
|
||||
"client: nuxt\n"+
|
||||
"keep_request_headers:\n"+
|
||||
" - Authorization\n"+
|
||||
" - Cookie\n"+
|
||||
" - Content-Type\n"+
|
||||
"keep_response_headers:\n"+
|
||||
" - Content-Type\n"+
|
||||
" - Location\n"+
|
||||
" - Set-Cookie\n"+
|
||||
"routes:\n"+
|
||||
" - method: POST\n"+
|
||||
" path: /login\n"+
|
||||
" capture:\n"+
|
||||
" - from: response.json\n"+
|
||||
" path: $.token\n"+
|
||||
" as: jwt:alice\n"+
|
||||
" category: jwt\n"+
|
||||
" - method: GET\n"+
|
||||
" path: /me\n"+
|
||||
" - method: GET\n"+
|
||||
" path: /api\n"+
|
||||
" capture:\n"+
|
||||
" - from: request.query\n"+
|
||||
" name: token\n"+
|
||||
" as: token:mcp-read\n"+
|
||||
" category: token\n"+
|
||||
" - method: POST\n"+
|
||||
" path: /token\n"+
|
||||
" capture:\n"+
|
||||
" - from: request.form\n"+
|
||||
" name: client_secret\n"+
|
||||
" as: oauth:secret\n"+
|
||||
" category: oauth_secret\n"+
|
||||
" - method: GET\n"+
|
||||
" path: /authorize\n"+
|
||||
" capture:\n"+
|
||||
" - from: response.location.query\n"+
|
||||
" name: code\n"+
|
||||
" as: oauth:code\n"+
|
||||
" category: oauth_code\n"+
|
||||
" - method: GET\n"+
|
||||
" path: /cookie\n"+
|
||||
" capture:\n"+
|
||||
" - from: request.header\n"+
|
||||
" name: Cookie\n"+
|
||||
" as: cookie:auth\n"+
|
||||
" category: cookie\n")
|
||||
proxy, err := NewProxy(ProxyConfig{
|
||||
Listen: "127.0.0.1:0",
|
||||
Upstream: upstream.URL,
|
||||
Fixtures: fixtures,
|
||||
VarsPath: varsPath,
|
||||
Rules: rules,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := httptest.NewServer(proxy.Handler())
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
post(t, srv.URL+"/login", "creds", "application/json", `{}`)
|
||||
me, err := http.NewRequest(http.MethodGet, srv.URL+"/me", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
me.Header.Set(SessionHeader, "creds")
|
||||
me.Header.Set("Authorization", "Bearer "+testJWT)
|
||||
meResp, err := http.DefaultClient.Do(me)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meResp.Body.Close()
|
||||
|
||||
q, err := http.NewRequest(http.MethodGet, srv.URL+"/api?token="+invTok, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q.Header.Set(SessionHeader, "creds")
|
||||
qResp, err := http.DefaultClient.Do(q)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
qResp.Body.Close()
|
||||
|
||||
post(t, srv.URL+"/token", "creds", "application/x-www-form-urlencoded", "client_secret="+secret)
|
||||
get, err := http.NewRequest(http.MethodGet, srv.URL+"/authorize", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
get.Header.Set(SessionHeader, "creds")
|
||||
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
aResp, err := client.Do(get)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aResp.Body.Close()
|
||||
|
||||
ck, err := http.NewRequest(http.MethodGet, srv.URL+"/cookie", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ck.Header.Set(SessionHeader, "creds")
|
||||
ck.Header.Set("Cookie", "auth_token="+cookieV)
|
||||
cResp, err := http.DefaultClient.Do(ck)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cResp.Body.Close()
|
||||
|
||||
if err := proxy.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(fixtures, "nuxt", "creds.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(raw)
|
||||
for _, secret := range []string{testJWT, invTok, secret, code, cookieV, "auth_token=" + cookieV} {
|
||||
if strings.Contains(text, secret) {
|
||||
t.Fatalf("secret %q leaked into YAML:\n%s", secret, text)
|
||||
}
|
||||
}
|
||||
for _, ph := range []string{"{{jwt:alice}}", "{{token:mcp-read}}", "{{oauth:secret}}", "{{oauth:code}}", "{{cookie:auth}}"} {
|
||||
if !strings.Contains(text, ph) {
|
||||
t.Fatalf("missing placeholder %s:\n%s", ph, text)
|
||||
}
|
||||
}
|
||||
|
||||
unclassified := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"token":"` + testJWT + `"}`))
|
||||
}))
|
||||
t.Cleanup(unclassified.Close)
|
||||
badFix := t.TempDir()
|
||||
badProxy, err := NewProxy(ProxyConfig{
|
||||
Listen: "127.0.0.1:0",
|
||||
Upstream: unclassified.URL,
|
||||
Fixtures: badFix,
|
||||
Rules: mustParseRules(t, testRulesYAML()),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
badSrv := httptest.NewServer(badProxy.Handler())
|
||||
t.Cleanup(badSrv.Close)
|
||||
breq, err := http.NewRequest(http.MethodGet, badSrv.URL+"/sample", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
breq.Header.Set(SessionHeader, "leak")
|
||||
bresp, err := http.DefaultClient.Do(breq)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bresp.Body.Close()
|
||||
if _, err := os.Stat(filepath.Join(badFix, "nuxt", "leak.yaml")); !os.IsNotExist(err) {
|
||||
t.Fatalf("unclassified jwt fixture committed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user