fix: recover introspection block across named fragment spreads; dedup cache hash; fix cache Set counter race

- graphql.go: parseGraphQLQuery now indexes top-level FragmentDefinition nodes
  and checkSelections resolves FragmentSpread by name, closing an
  introspection-blocking bypass (`{ ...F }` where F spreads __schema was
  allowed because the spread carried no selection set and definitions loop
  skipped fragments).
- proxy.go/server.go: handleCaching stores the computed cache hash on the
  request context; performProxyRequest reuses it instead of recomputing a
  full MD5 of the request body per proxied request (fallback preserved for
  direct callers).
- cache/memory: Set serializes the exists-check + counter update + store
  with the embedded mutex so concurrent Sets of the same new key can't
  double-increment entryCount/memoryUsage; regression test added.

Verification: go build, go vet, gofmt clean; full go test ./... and
go test -race ./... green (9 packages); golangci-lint 0 issues; trufflehog clean.
This commit is contained in:
2026-08-13 09:07:52 +01:00
parent 14be2398e8
commit 56eb0af213
6 changed files with 117 additions and 14 deletions
+31 -1
View File
@@ -476,6 +476,30 @@ func (suite *Tests) Test_DeepIntrospectionQueries() {
allowed: []string{},
expected: true,
},
{
name: "named fragment spreading introspection is blocked",
query: "query Q { ...IntroFields } fragment IntroFields on Query { __schema { queryType { name } } }",
allowed: []string{},
expected: true,
},
{
name: "named fragment spreading schema with allowlist is blocked",
query: "query Q { ...IntroFields } fragment IntroFields on Query { __schema { queryType { name } } }",
allowed: []string{"__typename"},
expected: true,
},
{
name: "named fragment without introspection is allowed",
query: "query Q { ...Fields } fragment Fields on User { id name }",
allowed: []string{},
expected: false,
},
{
name: "nested named fragment chain with introspection is blocked",
query: "query Q { ...A } fragment A on Query { ...B } fragment B on Query { __schema { queryType { name } } }",
allowed: []string{},
expected: true,
},
}
for _, tt := range tests {
@@ -592,9 +616,15 @@ func TestIntrospectionQueryHandling(t *testing.T) {
// Check selections
var blocked bool
fragments := make(map[string]*ast.FragmentDefinition)
for _, def := range p.Definitions {
if fd, ok := def.(*ast.FragmentDefinition); ok && fd.Name != nil {
fragments[fd.Name.Value] = fd
}
}
for _, def := range p.Definitions {
if op, ok := def.(*ast.OperationDefinition); ok {
blocked = checkSelections(ctx, op.GetSelectionSet().Selections)
blocked = checkSelections(ctx, op.GetSelectionSet().Selections, fragments)
break
}
}