Fix query introspection blocking on deeply nested types.

This commit is contained in:
2024-12-06 11:04:26 +00:00
parent cbe2afe539
commit fc9bab47fb
3 changed files with 126 additions and 121 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ func cleanEvents(pool *pgxpool.Pool) {
Message: "Failed to execute some queries", Message: "Failed to execute some queries",
Pairs: map[string]interface{}{ Pairs: map[string]interface{}{
"failed_queries": failedQueries, "failed_queries": failedQueries,
"errors": errMsgs, "errors": errMsgs,
}, },
}) })
} }
+123 -120
View File
@@ -66,163 +66,166 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
m := queryPool.Get().(map[string]interface{}) m := queryPool.Get().(map[string]interface{})
defer func() { defer func() {
for k := range m { for k := range m {
delete(m, k) delete(m, k)
} }
queryPool.Put(m) queryPool.Put(m)
}() }()
if err := json.Unmarshal(c.Body(), &m); err != nil { if err := json.Unmarshal(c.Body(), &m); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't unmarshal the request", Message: "Can't unmarshal the request",
Pairs: map[string]interface{}{"error": err.Error(), "body": string(c.Body())}, Pairs: map[string]interface{}{"error": err.Error(), "body": string(c.Body())},
}) })
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
} }
if res.shouldBlock {
resultPool.Put(res)
return res return res
}
return res
} }
query, ok := m["query"].(string) query, ok := m["query"].(string)
if !ok { if !ok {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't find the query", Message: "Can't find the query",
Pairs: map[string]interface{}{"m_val": m}, Pairs: map[string]interface{}{"m_val": m},
}) })
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
} }
resultPool.Put(res) return res
return res
} }
p, err := parser.Parse(parser.ParseParams{Source: query}) p, err := parser.Parse(parser.ParseParams{Source: query})
if err != nil { if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the query", Message: "Can't parse the query",
Pairs: map[string]interface{}{"query": query, "m_val": m}, Pairs: map[string]interface{}{"query": query, "m_val": m},
}) })
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
} }
resultPool.Put(res) return res
return res
} }
res.shouldIgnore = false res.shouldIgnore = false
res.operationName = "undefined" res.operationName = "undefined"
for _, d := range p.Definitions { for _, d := range p.Definitions {
if oper, ok := d.(*ast.OperationDefinition); ok { if oper, ok := d.(*ast.OperationDefinition); ok {
if res.operationType == "" { if res.operationType == "" {
res.operationType = strings.ToLower(oper.Operation) res.operationType = strings.ToLower(oper.Operation)
if oper.Name != nil { if oper.Name != nil {
res.operationName = oper.Name.Value res.operationName = oper.Name.Value
}
}
if cfg.Server.HostGraphQLReadOnly != "" {
if res.operationType == "" {
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
} else if res.operationType != "mutation" {
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
}
}
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Endpoint selection",
Pairs: map[string]interface{}{
"operationType": res.operationType,
"selectedEndpoint": res.activeEndpoint,
},
})
if res.operationType == "mutation" && cfg.Server.ReadOnlyMode {
cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Mutation blocked - server in read-only mode",
Pairs: map[string]interface{}{"query": query},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
_ = c.Status(403).SendString("The server is in read-only mode")
res.shouldBlock = true
resultPool.Put(res)
return res
}
for _, dir := range oper.Directives {
if dir.Name.Value == "cached" {
res.cacheRequest = true
for _, arg := range dir.Arguments {
switch arg.Name.Value {
case "ttl":
if v, ok := arg.Value.GetValue().(string); ok {
res.cacheTime, _ = strconv.Atoi(v)
} }
case "refresh":
if v, ok := arg.Value.GetValue().(bool); ok {
res.cacheRefresh = v
}
}
} }
}
}
if cfg.Security.BlockIntrospection { if cfg.Server.HostGraphQLReadOnly != "" {
res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections) if res.operationType == "" || res.operationType != "mutation" {
if res.shouldBlock { res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
resultPool.Put(res) }
return res }
}
if res.operationType == "mutation" && cfg.Server.ReadOnlyMode {
cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Mutation blocked - server in read-only mode",
Pairs: map[string]interface{}{"query": query},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
_ = c.Status(403).SendString("The server is in read-only mode")
res.shouldBlock = true
resultPool.Put(res)
return res
}
for _, dir := range oper.Directives {
if dir.Name.Value == "cached" {
res.cacheRequest = true
for _, arg := range dir.Arguments {
switch arg.Name.Value {
case "ttl":
if v, ok := arg.Value.GetValue().(string); ok {
res.cacheTime, _ = strconv.Atoi(v)
}
case "refresh":
if v, ok := arg.Value.GetValue().(bool); ok {
res.cacheRefresh = v
}
}
}
}
}
if cfg.Security.BlockIntrospection {
if checkSelections(c, oper.GetSelectionSet().Selections) {
_ = c.Status(403).SendString("Introspection queries are not allowed")
res.shouldBlock = true
resultPool.Put(res)
return res
}
}
} }
}
} }
return res return res
} }
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool { func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
stack := make([]ast.Selection, len(selections)) for _, s := range selections {
copy(stack, selections) switch sel := s.(type) {
case *ast.Field:
for len(stack) > 0 { fieldName := strings.ToLower(sel.Name.Value)
var s ast.Selection if _, exists := introspectionQueries[fieldName]; exists {
s, stack = stack[len(stack)-1], stack[:len(stack)-1] if len(cfg.Security.IntrospectionAllowed) > 0 {
if _, allowed := introspectionAllowedQueries[fieldName]; !allowed {
if field, ok := s.(*ast.Field); ok { return true
if checkIfContainsIntrospection(c, field.Name.Value) { }
return true } else {
return true
}
}
if sel.SelectionSet != nil {
if checkSelections(c, sel.GetSelectionSet().Selections) {
return true
}
}
} }
if field.SelectionSet != nil {
stack = append(stack, field.GetSelectionSet().Selections...)
}
}
} }
return false return false
} }
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) bool { func checkIfContainsIntrospection(c *fiber.Ctx, query string) bool {
whateverLower := strings.ToLower(whatever) blocked := false
// Try parsing as a complete query first
if _, exists := introspectionQueries[whateverLower]; exists { p, err := parser.Parse(parser.ParseParams{Source: query})
if len(cfg.Security.IntrospectionAllowed) > 0 { if err == nil {
if _, allowed := introspectionAllowedQueries[whateverLower]; allowed { // It's a complete query, check all selections
cfg.Logger.Debug(&libpack_logger.LogMessage{ for _, def := range p.Definitions {
Message: "Introspection query allowed, passing through", if op, ok := def.(*ast.OperationDefinition); ok {
Pairs: map[string]interface{}{"query": whatever}, if op.SelectionSet != nil {
}) blocked = checkSelections(c, op.GetSelectionSet().Selections)
return false }
} }
} }
} else {
// Not a complete query, check as a field name
whateverLower := strings.ToLower(query)
if _, exists := introspectionQueries[whateverLower]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 {
if _, allowed := introspectionAllowedQueries[whateverLower]; !allowed {
blocked = true
}
} else {
blocked = true
}
}
}
if blocked {
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
} }
_ = c.Status(403).SendString("Introspection queries are not allowed") _ = c.Status(403).SendString("Introspection queries are not allowed")
return true
} }
return false return blocked
} }
+2
View File
@@ -408,6 +408,8 @@ func (suite *Tests) Test_checkIfContainsIntrospection() {
{"allowed introspection", "__schema", []string{"__schema"}, false}, {"allowed introspection", "__schema", []string{"__schema"}, false},
{"disallowed introspection", "__type", []string{"__schema"}, true}, {"disallowed introspection", "__type", []string{"__schema"}, true},
{"non-introspection query", "normalQuery", []string{}, false}, {"non-introspection query", "normalQuery", []string{}, false},
{"allowed introspection with deep nesting of __typename", "{__schema {queryType {fields {name description __typename}}}}", []string{"__schema", "__typename"}, false},
{"disallowed introspection with deep nesting of __typename", "{__type {queryType {fields {name description __typename}}}}", []string{"__type"}, true},
} }
for _, tt := range tests { for _, tt := range tests {