mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-20 06:28:13 +00:00
Fix query introspection blocking on deeply nested types.
This commit is contained in:
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user