Compare commits

...

16 Commits

Author SHA1 Message Date
lukaszraczylo e54bbe8249 Additional tests to ensure that schema introspection is working as expected 2024-12-06 12:03:37 +00:00
lukaszraczylo ed3966e577 If the field is allowed, continue checking remaining fields. 2024-12-06 11:58:34 +00:00
lukaszraczylo 6a52a9f673 Fixes the issue with case comparison. 2024-12-06 11:49:47 +00:00
lukaszraczylo 1ca05a7a2a Release 0.24.x - changes the query introspection. 2024-12-06 11:27:01 +00:00
lukaszraczylo eb1b4b4eb7 Enhance the tests to cover the end status code as well. 2024-12-06 11:15:38 +00:00
lukaszraczylo fc9bab47fb Fix query introspection blocking on deeply nested types. 2024-12-06 11:04:26 +00:00
lukaszraczylo cbe2afe539 Gather cleaner event errors and display as a group rather than separately. 2024-12-06 09:39:26 +00:00
lukaszraczylo 2190744729 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-06 03:14:57 +00:00
lukaszraczylo 0a96d139b6 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-05 03:15:42 +00:00
lukaszraczylo 1c1ac06e11 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-10 03:06:36 +00:00
lukaszraczylo b2a67df3b6 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-09 03:03:58 +00:00
lukaszraczylo 3805e63f95 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-08 03:04:48 +00:00
lukaszraczylo 8abf731867 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-29 03:08:04 +00:00
lukaszraczylo 4e9db9a5c7 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-18 03:07:24 +00:00
lukaszraczylo 615836ab36 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-15 03:08:28 +00:00
lukaszraczylo a51f37c0a2 Update go.mod and go.sum
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-12 03:04:33 +00:00
8 changed files with 412 additions and 79 deletions
+2
View File
@@ -40,6 +40,8 @@ I wanted to monitor the queries and responses of our graphql endpoint. Still, we
You should always try to stick to the latest and greatest version of the graphql-proxy to ensure that it's as much bug-free as possible. Following list will be kept to the maximum of five "most important" bugs and enhancements included in the latest versions.
* **06/12/2024 - 0.24.2** - Fixes the bug where deeply nested introspection queries were blocked despite of being present on the whitelist. GraphQL proxy will now inspect the queries in depth to find any possible nested introspections.
* **20/08/2024 - 0.23.21+** - Fixes the bug when timeouts were not respected on proxy-graphql line. Affected versions before that were timeouting after 30 seconds which was set as default ( thanks to Jurica Železnjak for reporting ). It also provides a temporary fix for running within kubernetes deployment, when graphql server ( for example - hasura ) took more time to start than the proxy, causing avalanche of errors with "can't proxy the request".
* **19/08/2024 - 0.21.82+** - Fixed the issue when proxy failed to start if global cache was disabled, therefore not initialized and proxy tried to perform the cache operations during normal query operations.
+19 -4
View File
@@ -71,13 +71,14 @@ func enableHasuraEventCleaner() {
func cleanEvents(pool *pgxpool.Pool) {
ctx := context.Background()
var errors []error
var failedQueries []string
for _, query := range delQueries {
_, err := pool.Exec(ctx, fmt.Sprintf(query, cfg.HasuraEventCleaner.ClearOlderThan))
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Failed to execute query",
Pairs: map[string]interface{}{"query": query, "error": err.Error()},
})
errors = append(errors, err)
failedQueries = append(failedQueries, query)
} else {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Successfully executed query",
@@ -85,4 +86,18 @@ func cleanEvents(pool *pgxpool.Pool) {
})
}
}
if len(errors) > 0 {
var errMsgs []string
for _, err := range errors {
errMsgs = append(errMsgs, err.Error())
}
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Failed to execute some queries",
Pairs: map[string]interface{}{
"failed_queries": failedQueries,
"errors": errMsgs,
},
})
}
}
+10 -10
View File
@@ -15,10 +15,10 @@ require (
github.com/jackc/pgx/v5 v5.7.1
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
github.com/lukaszraczylo/go-ratecounter v0.1.12
github.com/lukaszraczylo/go-simple-graphql v1.2.29
github.com/redis/go-redis/v9 v9.6.1
github.com/lukaszraczylo/go-simple-graphql v1.2.32
github.com/redis/go-redis/v9 v9.7.0
github.com/stretchr/testify v1.9.0
github.com/valyala/fasthttp v1.56.0
github.com/valyala/fasthttp v1.57.0
)
require (
@@ -31,7 +31,7 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.17.10 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -45,11 +45,11 @@ require (
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/crypto v0.30.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+20 -20
View File
@@ -44,8 +44,8 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -54,8 +54,8 @@ github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9 h1:pL8B9mjv6RPUf
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c=
github.com/lukaszraczylo/go-ratecounter v0.1.12 h1:VO6hHYGw/Jy9JUizXf/bS0AI2QX1ueWWAWckMFVJ/w4=
github.com/lukaszraczylo/go-ratecounter v0.1.12/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
github.com/lukaszraczylo/go-simple-graphql v1.2.29 h1:Fo/3SN4vrST1pyX1UJ5Nd+pQCkurZNJSck4pyx5B/Fk=
github.com/lukaszraczylo/go-simple-graphql v1.2.29/go.mod h1:kCvRu01tLxj0iKash5qwL7Em+SltQmZ82bs0yu2aOrk=
github.com/lukaszraczylo/go-simple-graphql v1.2.32 h1:CKjXgNHUuwzeBVKIPLXoMw4wPQCqchIV0htFGk5+Hpg=
github.com/lukaszraczylo/go-simple-graphql v1.2.32/go.mod h1:Y0fEHnPijfPyTF4fzSEpgmu5kaA4lENA0+gQdI1y1+0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -65,8 +65,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -79,8 +79,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg=
github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
@@ -93,22 +93,22 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+59 -44
View File
@@ -28,7 +28,7 @@ var (
func prepareQueriesAndExemptions() {
for _, q := range cfg.Security.IntrospectionAllowed {
introspectionAllowedQueries[strings.ToLower(q)] = struct{}{}
introspectionAllowedQueries[strings.ToLower(strings.TrimSpace(q))] = struct{}{}
}
for _, u := range cfg.Server.AllowURLs {
@@ -80,10 +80,6 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
if res.shouldBlock {
resultPool.Put(res)
return res
}
return res
}
@@ -96,7 +92,6 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
resultPool.Put(res)
return res
}
@@ -109,7 +104,6 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
resultPool.Put(res)
return res
}
@@ -126,21 +120,11 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
}
if cfg.Server.HostGraphQLReadOnly != "" {
if res.operationType == "" {
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
} else if res.operationType != "mutation" {
if res.operationType == "" || 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",
@@ -174,8 +158,9 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
}
if cfg.Security.BlockIntrospection {
res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections)
if res.shouldBlock {
if checkSelections(c, oper.GetSelectionSet().Selections) {
_ = c.Status(403).SendString("Introspection queries are not allowed")
res.shouldBlock = true
resultPool.Put(res)
return res
}
@@ -186,43 +171,73 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
}
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
stack := make([]ast.Selection, len(selections))
copy(stack, selections)
for len(stack) > 0 {
var s ast.Selection
s, stack = stack[len(stack)-1], stack[:len(stack)-1]
if field, ok := s.(*ast.Field); ok {
if checkIfContainsIntrospection(c, field.Name.Value) {
for _, s := range selections {
switch sel := s.(type) {
case *ast.Field:
fieldName := strings.ToLower(sel.Name.Value)
if _, exists := introspectionQueries[fieldName]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 {
// If this field is allowed, don't block and continue checking other fields
if _, allowed := introspectionAllowedQueries[fieldName]; allowed {
if sel.SelectionSet != nil {
if checkSelections(c, sel.GetSelectionSet().Selections) {
return true
}
}
continue
}
return true
}
return true
}
if field.SelectionSet != nil {
stack = append(stack, field.GetSelectionSet().Selections...)
if sel.SelectionSet != nil {
if checkSelections(c, sel.GetSelectionSet().Selections) {
return true
}
}
case *ast.InlineFragment:
if sel.SelectionSet != nil {
if checkSelections(c, sel.GetSelectionSet().Selections) {
return true
}
}
}
}
return false
}
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) bool {
whateverLower := strings.ToLower(whatever)
if _, exists := introspectionQueries[whateverLower]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 {
if _, allowed := introspectionAllowedQueries[whateverLower]; allowed {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Introspection query allowed, passing through",
Pairs: map[string]interface{}{"query": whatever},
})
return false
func checkIfContainsIntrospection(c *fiber.Ctx, query string) bool {
blocked := false
// Try parsing as a complete query first
p, err := parser.Parse(parser.ParseParams{Source: query})
if err == nil {
// It's a complete query, check all selections
for _, def := range p.Definitions {
if op, ok := def.(*ast.OperationDefinition); ok {
if op.SelectionSet != nil {
blocked = checkSelections(c, op.GetSelectionSet().Selections)
}
}
}
} 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() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
_ = c.Status(403).SendString("Introspection queries are not allowed")
return true
}
return false
return blocked
}
+179
View File
@@ -3,8 +3,12 @@ package main
import (
"fmt"
"strings"
"testing"
"github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2"
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/parser"
"github.com/valyala/fasthttp"
)
@@ -408,6 +412,8 @@ func (suite *Tests) Test_checkIfContainsIntrospection() {
{"allowed introspection", "__schema", []string{"__schema"}, false},
{"disallowed introspection", "__type", []string{"__schema"}, true},
{"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 {
@@ -430,3 +436,176 @@ func createTestContext(body string) *fiber.Ctx {
ctx.Request().SetBody([]byte(body))
return ctx
}
func (suite *Tests) Test_DeepIntrospectionQueries() {
tests := []struct {
name string
query string
allowed []string
expected bool
}{
{
name: "deeply nested single introspection",
query: "query { users { profiles { settings { preferences { __typename } } } } }",
allowed: []string{},
expected: true,
},
{
name: "multiple nested introspections",
query: "query { users { __typename profiles { __schema settings { __type } } } }",
allowed: []string{},
expected: true,
},
{
name: "nested with selective allowlist",
query: "query { users { __typename profiles { __schema settings { __type } } } }",
allowed: []string{"__typename"},
expected: true,
},
{
name: "deeply nested with full allowlist",
query: "query { users { __typename profiles { __schema settings { __type } } } }",
allowed: []string{"__typename", "__schema", "__type"},
expected: false,
},
{
name: "deeply nested with repeated item from allowlist",
query: "query PreloadStaticData {\n scenario {\n id\n name\n __typename\n }\n impact {\n id\n description\n __typename\n }\n likelihood {\n id\n description\n __typename\n }\n consequence {\n name\n __typename\n }\n risk_categories {\n name\n abbreviation\n __typename\n }\n mitigation {\n name\n __typename\n }\n}",
allowed: []string{"__type", "__typename"},
expected: false,
},
{
name: "deeply nested with repeated item denied",
query: "query PreloadStaticData {\n scenario {\n id\n name\n __typename\n }\n impact {\n id\n description\n __typename\n }\n likelihood {\n id\n description\n __typename\n }\n consequence {\n name\n __typename\n }\n risk_categories {\n name\n abbreviation\n __typename\n }\n mitigation {\n name\n __typename\n }\n}",
allowed: []string{},
expected: true,
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
cfg.Security.BlockIntrospection = true
cfg.Security.IntrospectionAllowed = tt.allowed
introspectionAllowedQueries = make(map[string]struct{})
for _, q := range tt.allowed {
introspectionAllowedQueries[strings.ToLower(q)] = struct{}{}
}
body := map[string]interface{}{
"query": tt.query,
}
bodyBytes, _ := json.Marshal(body)
ctx := fiber.New().AcquireCtx(&fasthttp.RequestCtx{})
ctx.Request().SetBody(bodyBytes)
parseGraphQLQuery(ctx)
if tt.expected {
suite.Equal(403, ctx.Response().StatusCode())
} else {
suite.Equal(200, ctx.Response().StatusCode())
}
})
}
}
func TestIntrospectionQueryHandling(t *testing.T) {
tests := []struct {
name string
blockIntrospection bool
allowedQueries []string
query string
wantBlocked bool
}{
{
name: "allows __typename when in allowed list",
blockIntrospection: true,
allowedQueries: []string{"__typename"},
query: `{
users {
id
name
__typename
}
}`,
wantBlocked: false,
},
{
name: "case insensitive matching for allowed queries",
blockIntrospection: true,
allowedQueries: []string{"__TYPENAME"},
query: `{
users {
__typename
}
}`,
wantBlocked: false,
},
{
name: "blocks other introspection queries",
blockIntrospection: true,
allowedQueries: []string{"__typename"},
query: `{
__schema {
types {
name
}
}
}`,
wantBlocked: true,
},
{
name: "allows multiple __typename occurrences",
blockIntrospection: true,
allowedQueries: []string{"__typename"},
query: `{
users {
__typename
posts {
__typename
}
}
}`,
wantBlocked: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup config
cfg = &config{
Security: struct {
IntrospectionAllowed []string
BlockIntrospection bool
}{
IntrospectionAllowed: tt.allowedQueries,
BlockIntrospection: tt.blockIntrospection,
},
}
// Initialize allowed queries
prepareQueriesAndExemptions()
// Parse query
p, err := parser.Parse(parser.ParseParams{Source: tt.query})
if err != nil {
t.Fatalf("failed to parse query: %v", err)
}
// Create mock fiber context
app := fiber.New()
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)
// Check selections
var blocked bool
for _, def := range p.Definitions {
if op, ok := def.(*ast.OperationDefinition); ok {
blocked = checkSelections(ctx, op.GetSelectionSet().Selections)
break
}
}
if blocked != tt.wantBlocked {
t.Errorf("checkSelections() blocked = %v, want %v", blocked, tt.wantBlocked)
}
})
}
}
+1 -1
View File
@@ -125,7 +125,7 @@ func parseConfig() {
go enableApi()
go enableHasuraEventCleaner()
})
prepareQueriesAndExemptions() // Ensure this function is defined elsewhere
prepareQueriesAndExemptions()
}
func main() {
+122
View File
@@ -1,6 +1,7 @@
package main
import (
"fmt"
"os"
"testing"
"time"
@@ -11,6 +12,7 @@ import (
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
assertions "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
)
type Tests struct {
@@ -138,3 +140,123 @@ func (suite *Tests) Test_getDetailsFromEnv() {
})
}
}
func TestIntrospectionEnvironmentConfig(t *testing.T) {
// Save original env vars
oldEnv := make(map[string]string)
varsToSave := []string{
"BLOCK_SCHEMA_INTROSPECTION",
"ALLOWED_INTROSPECTION",
"GMP_BLOCK_SCHEMA_INTROSPECTION",
"GMP_ALLOWED_INTROSPECTION",
}
for _, env := range varsToSave {
if val, exists := os.LookupEnv(env); exists {
oldEnv[env] = val
os.Unsetenv(env)
}
}
defer func() {
// Restore original env vars
for k, v := range oldEnv {
os.Setenv(k, v)
}
}()
tests := []struct {
name string
envVars map[string]string
query string
wantBlocked bool
wantEndpoint string
}{
{
name: "basic typename allowed",
envVars: map[string]string{
"BLOCK_SCHEMA_INTROSPECTION": "true",
"ALLOWED_INTROSPECTION": "__typename",
},
query: `{
users {
id
__typename
}
}`,
wantBlocked: false,
},
{
name: "GMP prefix takes precedence",
envVars: map[string]string{
"BLOCK_SCHEMA_INTROSPECTION": "false",
"GMP_BLOCK_SCHEMA_INTROSPECTION": "true",
"ALLOWED_INTROSPECTION": "__type",
"GMP_ALLOWED_INTROSPECTION": "__typename",
},
query: `{
users {
__typename
}
}`,
wantBlocked: false,
},
{
name: "multiple allowed queries",
envVars: map[string]string{
"BLOCK_SCHEMA_INTROSPECTION": "true",
"ALLOWED_INTROSPECTION": "__typename,__schema",
},
query: `{
__schema {
types {
name
__typename
}
}
}`,
wantBlocked: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set test env vars
for k, v := range tt.envVars {
os.Setenv(k, v)
}
// Reset global config
cfg = nil
parseConfig()
// Create test request
app := fiber.New()
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)
ctx.Request().Header.SetMethod("POST")
ctx.Request().SetBody([]byte(fmt.Sprintf(`{"query": %q}`, tt.query)))
result := parseGraphQLQuery(ctx)
if result.shouldBlock != tt.wantBlocked {
t.Errorf("query blocked = %v, want %v", result.shouldBlock, tt.wantBlocked)
}
// Clean up test env vars
for k := range tt.envVars {
os.Unsetenv(k)
}
})
}
}
func TestMain(m *testing.M) {
// Setup test environment
os.Setenv("LOG_LEVEL", "error") // Reduce noise in tests
// Run tests
code := m.Run()
// Cleanup
os.Unsetenv("LOG_LEVEL")
os.Exit(code)
}