Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query runner: Block known problematic functions if found in parse tree #660

Merged
merged 4 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ require (
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0
google.golang.org/api v0.143.0
google.golang.org/protobuf v1.33.0
google.golang.org/protobuf v1.36.1
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
Expand Down
79 changes: 70 additions & 9 deletions input/postgres/explain_analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import (
"context"
"database/sql"
"fmt"
"slices"
"strings"

"github.com/guregu/null"
"github.com/lib/pq"
"github.com/pganalyze/collector/util"
pg_query "github.com/pganalyze/pg_query_go/v6"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protopath"
"google.golang.org/protobuf/reflect/protorange"
"google.golang.org/protobuf/reflect/protoreflect"
)

func RunExplainAnalyzeForQueryRun(ctx context.Context, db *sql.DB, query string, parameters []null.String, parameterTypes []string, marker string) (result string, err error) {
Expand Down Expand Up @@ -53,16 +58,72 @@ func runExplainAnalyze(ctx context.Context, db *sql.DB, query string, parameters
}

func validateQuery(query string) error {
var isUtil []bool
// To be on the safe side never EXPLAIN a statement that can't be parsed,
// or multiple statements in one (leading to accidental execution)
isUtil, err := util.IsUtilityStmt(query)
if err != nil || len(isUtil) != 1 || isUtil[0] {
err = fmt.Errorf("query is not permitted to run (multi-statement or utility command?)")
return err
parseResult, err := pg_query.Parse(query)
if err != nil {
return fmt.Errorf("query is not permitted to run - failed to parse")
}
if len(parseResult.Stmts) != 1 {
return fmt.Errorf("query is not permitted to run - multi-statement query string")
}

// TODO: Consider adding additional checks here (e.g. blocking known bad function calls)
stmt := parseResult.Stmts[0].Stmt.Node
switch stmt.(type) {
case *pg_query.Node_SelectStmt:
// Allowed, continue
// Note that we permit wCTEs here (for now), and instead rely on the read-only transaction to block them
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked a bit offline too, but we could potentially do these checks in the below treewalk so that we can also check wCTEs. I think it's fine as is for now, but we could come back later potentially.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, lets revise that at a later point, but agreed we could adjust this to match the check we do on the server side for wCTEs.

case *pg_query.Node_InsertStmt, *pg_query.Node_UpdateStmt, *pg_query.Node_DeleteStmt:
return fmt.Errorf("query is not permitted to run - DML statement")
default:
return fmt.Errorf("query is not permitted to run - utility statement")
}

err = validateBlockedFunctions(parseResult)
if err != nil {
return err
}

return nil
}

var blockedFunctions = []string{
// Blocked because these functions allow exfiltrating data to external servers
"dblink",
"dblink_connect",
"dblink_exec",
// Blocked because these functions allow executing arbitrary SQL as input (which can workaround other checks)
"xpath_table",
lfittl marked this conversation as resolved.
Show resolved Hide resolved
}

func validateBlockedFunctions(parseResult *pg_query.ParseResult) error {
return walkParseTree(parseResult, func(nodeType string, node proto.Message) error {
if nodeType != "FuncCall" {
return nil
}

f := node.(*pg_query.FuncCall)
// The funcname field can be optionally schema qualified, so we take the last item in the list of names
nameNode := f.Funcname[len(f.Funcname)-1]
name := nameNode.GetString_().Sval

if slices.Contains(blockedFunctions, name) {
return fmt.Errorf("query is not permitted to run - function not allowed: %s", name)
}
return nil
})
}

type treeWalker func(nodeType string, node proto.Message) error

func walkParseTree(parseResult *pg_query.ParseResult, fn treeWalker) error {
return protorange.Range(parseResult.ProtoReflect(), func(p protopath.Values) error {
last := p.Index(-1)
m, ok := last.Value.Interface().(protoreflect.Message)
if ok {
err := fn(string(m.Descriptor().Name()), m.Interface())
if err != nil {
return err
}
}
return nil
})
}
11 changes: 9 additions & 2 deletions input/postgres/explain_analyze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,21 @@ var queryRunTests = []queryRunTestpair{
[]null.String{},
[]string{},
"",
"pq: cannot execute UPDATE in a read-only transaction",
"query is not permitted to run - DML statement",
},
{
"SELECT 1; UPDATE test SET id = 123",
[]null.String{},
[]string{},
"",
"query is not permitted to run (multi-statement or utility command?)",
"query is not permitted to run - multi-statement query string",
},
{
"SELECT dblink_exec('host=myhost user=myuser password=mypass dbname=mydb', dblink_build_sql_insert('secret_table', '1', 1, '{\"1\"}', '{\"1\"}'))",
[]null.String{},
[]string{},
"",
"query is not permitted to run - function not allowed: dblink_exec",
},
{
"SELECT $1",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading