Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
21 changes: 16 additions & 5 deletions core/functions/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,23 @@ func (function Function) GetID() id.Id {
func (function Function) GetInnerDefinition() string {
// TODO: right now we're hardcode searching for $$, which will fail for some definition strings
start := strings.Index(function.Definition, "$$")
end := strings.LastIndex(function.Definition, "$$")
if start == -1 || end == -1 {
// Return the whole definition for now
return function.Definition
if start != -1 {
end := strings.LastIndex(function.Definition, "$$")
if end != -1 {
return strings.TrimSpace(function.Definition[start+2 : end])
}
}
return strings.TrimSpace(function.Definition[start+2 : end])

asQuote := strings.Index(strings.ToLower(function.Definition), "as '")
if asQuote != -1 {
endQuote := strings.LastIndex(function.Definition, "'")
if endQuote != -1 {
return strings.TrimSpace(function.Definition[asQuote+4 : endQuote])
}
}

// Return the whole definition for now
return function.Definition
}

// ReplaceDefinition returns a new definition with the inner portion replaced with the given string.
Expand Down
2 changes: 1 addition & 1 deletion core/procedures/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func (pgp *Collection) iterateIDs(_ context.Context, callback func(procID id.Pro
}

// IterateProcedures iterates over all procedures in the collection.
func (pgp *Collection) IterateProcedures(_ context.Context, callback func(f Procedure) (stop bool, err error)) error {
func (pgp *Collection) IterateProcedures(_ context.Context, callback func(p Procedure) (stop bool, err error)) error {
for _, procID := range pgp.idCache {
stop, err := callback(pgp.accessCache[procID])
if err != nil {
Expand Down
162 changes: 160 additions & 2 deletions server/functions/iterate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import (
"github.com/dolthub/go-mysql-server/sql"

"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/functions"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/procedures"
"github.com/dolthub/doltgresql/core/sequences"
"github.com/dolthub/doltgresql/core/typecollection"
"github.com/dolthub/doltgresql/server/tables"
Expand All @@ -40,8 +42,12 @@ type Callbacks struct {
ColumnDefault func(ctx *sql.Context, schema ItemSchema, table ItemTable, check ItemColumnDefault) (cont bool, err error)
// ForeignKey is the callback for foreign keys.
ForeignKey func(ctx *sql.Context, schema ItemSchema, table ItemTable, foreignKey ItemForeignKey) (cont bool, err error)
// Function is the callback for functions.
Function func(ctx *sql.Context, schema ItemSchema, function ItemFunction) (cont bool, err error)
// Index is the callback for indexes.
Index func(ctx *sql.Context, schema ItemSchema, table ItemTable, index ItemIndex) (cont bool, err error)
// Procedure is the callbacks for procedures.
Procedure func(ctx *sql.Context, schema ItemSchema, function ItemProcedure) (cont bool, err error)
// Schema is the callback for schemas/namespaces.
Schema func(ctx *sql.Context, schema ItemSchema) (cont bool, err error)
// Sequence is the callback for sequences.
Expand Down Expand Up @@ -81,12 +87,24 @@ type ItemForeignKey struct {
Item sql.ForeignKeyConstraint
}

// ItemFunction contains the relevant information to pass to the Function callback.
type ItemFunction struct {
OID id.Function
Item *functions.Function
}

// ItemIndex contains the relevant information to pass to the Index callback.
type ItemIndex struct {
OID id.Index
Item sql.Index
}

// ItemProcedure contains the relevant information to pass to the Procedure callback.
type ItemProcedure struct {
OID id.Procedure
Item *procedures.Procedure
}

// ItemSchema contains the relevant information to pass to the Schema callback.
type ItemSchema struct {
OID id.Namespace
Expand Down Expand Up @@ -168,7 +186,26 @@ func IterateDatabase(ctx *sql.Context, database string, callbacks Callbacks) err
typeMap = typesBySchema(ctx, coll)
}

if err = iterateSchemas(ctx, callbacks, schemas, sequenceMap, typeMap); err != nil {
var functionMap map[string][]*functions.Function
if callbacks.Function != nil {
coll, err := core.GetFunctionsCollectionFromContext(ctx, database)
if err != nil {
return err
}
functionMap = functionsBySchema(ctx, coll)

}

var procedureMap map[string][]*procedures.Procedure
if callbacks.Function != nil {
coll, err := core.GetProceduresCollectionFromContext(ctx, database)
if err != nil {
return err
}
procedureMap = proceduresBySchema(ctx, coll)
}

if err = iterateSchemas(ctx, callbacks, schemas, sequenceMap, typeMap, functionMap, procedureMap); err != nil {
return err
}
}
Expand All @@ -185,6 +222,28 @@ func typesBySchema(ctx *sql.Context, coll *typecollection.TypeCollection) map[st
return m
}

// functionsBySchema returns a map of schema name to functions within that schema.
func functionsBySchema(ctx *sql.Context, coll *functions.Collection) map[string][]*functions.Function {
m := make(map[string][]*functions.Function)
_ = coll.IterateFunctions(ctx, func(f functions.Function) (stop bool, err error) {
sch := f.ID.SchemaName()
m[sch] = append(m[sch], &f)
return false, nil
})
return m
}

// proceduresBySchema returns a map of schema name to procedures within that schema.
func proceduresBySchema(ctx *sql.Context, coll *procedures.Collection) map[string][]*procedures.Procedure {
m := make(map[string][]*procedures.Procedure)
_ = coll.IterateProcedures(ctx, func(p procedures.Procedure) (stop bool, err error) {
sch := p.ID.SchemaName()
m[sch] = append(m[sch], &p)
return false, nil
})
return m
}

// IterateCurrentDatabase iterates over the current database, calling each callback as the relevant items are iterated
// over. This is a central function that homogenizes all iteration, since OIDs depend on a deterministic iteration over
// items. This function should be expanded as we add more items to iterate over.
Expand All @@ -199,6 +258,8 @@ func iterateSchemas(
sortedSchemas []sql.DatabaseSchema,
sequenceMap map[string][]*sequences.Sequence,
typeMap map[string][]*types.DoltgresType,
functionMap map[string][]*functions.Function,
procedureMap map[string][]*procedures.Procedure,
) error {
// Iterate over the sorted schemas by the iteration order
for _, schemaIndex := range callbacks.schemaIterationOrder(sortedSchemas) {
Expand Down Expand Up @@ -227,6 +288,16 @@ func iterateSchemas(
return err
}

err = iterateFunctions(ctx, callbacks, functionMap, schema, itemSchema)
if err != nil {
return err
}

err = iterateProcedures(ctx, callbacks, procedureMap, schema, itemSchema)
if err != nil {
return err
}

// Check if we need to iterate over tables
if callbacks.iteratesOverTables() {
tableNames, err := schema.GetTableNames(ctx)
Expand Down Expand Up @@ -446,6 +517,22 @@ func iterateForeignKeys(ctx *sql.Context, callbacks Callbacks, itemSchema ItemSc
return nil
}

// iterateFunctions is called by iterateSchemas to handle functions.
func iterateFunctions(ctx *sql.Context, callbacks Callbacks, functionMap map[string][]*functions.Function, schema sql.DatabaseSchema, itemSchema ItemSchema) error {
for _, f := range functionMap[schema.SchemaName()] {
itemFunction := ItemFunction{
OID: f.ID,
Item: f,
}
if cont, err := callbacks.Function(ctx, itemSchema, itemFunction); err != nil {
return err
} else if !cont {
return nil
}
}
return nil
}

// iterateIndexes is called by iterateTables to handle indexes.
func iterateIndexes(ctx *sql.Context, callbacks Callbacks, itemSchema ItemSchema, itemTable ItemTable, indexCount *int) error {
if indexedTable, ok := itemTable.Item.(sql.IndexAddressable); ok {
Expand All @@ -472,6 +559,22 @@ func iterateIndexes(ctx *sql.Context, callbacks Callbacks, itemSchema ItemSchema
return nil
}

// iterateProcedures is called by iterateSchemas to handle procedures.
func iterateProcedures(ctx *sql.Context, callbacks Callbacks, procedureMap map[string][]*procedures.Procedure, schema sql.DatabaseSchema, itemSchema ItemSchema) error {
for _, p := range procedureMap[schema.SchemaName()] {
itemProcedure := ItemProcedure{
OID: p.ID,
Item: p,
}
if cont, err := callbacks.Procedure(ctx, itemSchema, itemProcedure); err != nil {
return err
} else if !cont {
return nil
}
}
return nil
}

// RunCallback iterates over schemas, etc. to find the item that the given oid points to. Once the item has been found,
// the relevant callback is called with the item. This means that, at most, only one callback will be called. If the
// item cannot be found, then no callbacks are called.
Expand Down Expand Up @@ -512,6 +615,14 @@ func RunCallback(ctx *sql.Context, internalID id.Id, callbacks Callbacks) error
if !itemSchema.OID.IsValid() {
return nil
}
// Check if we're looking for a function
if internalID.Section() == id.Section_Function {
return runFunction(ctx, internalID, callbacks, itemSchema)
}
// Check if we're looking for a procedure
if internalID.Section() == id.Section_Procedure {
return runProcedure(ctx, internalID, callbacks, itemSchema)
}
// Check if we're looking for a sequence
if internalID.Section() == id.Section_Sequence {
return runSequence(ctx, internalID, callbacks, itemSchema)
Expand Down Expand Up @@ -691,6 +802,42 @@ func runNamespace(ctx *sql.Context, internalID id.Id, callbacks Callbacks, sorte
return nil
}

// runFunction is called by RunCallback to handle Section_Function.
func runFunction(ctx *sql.Context, internalID id.Id, callbacks Callbacks, itemSchema ItemSchema) error {
collection, err := core.GetFunctionsCollectionFromContext(ctx, itemSchema.Item.Name())
if err != nil {
return err
}
return collection.IterateFunctions(ctx, func(f functions.Function) (stop bool, err error) {
if f.ID.AsId() == internalID {
_, err = callbacks.Function(ctx, itemSchema, ItemFunction{
OID: id.Function(internalID),
Item: &f,
})
return true, err
}
return false, nil
})
}

// runProcedure is called by RunCallback to handle Section_Procedure.
func runProcedure(ctx *sql.Context, internalID id.Id, callbacks Callbacks, itemSchema ItemSchema) error {
collection, err := core.GetProceduresCollectionFromContext(ctx, itemSchema.Item.Name())
if err != nil {
return err
}
return collection.IterateProcedures(ctx, func(p procedures.Procedure) (stop bool, err error) {
if p.ID.AsId() == internalID {
_, err = callbacks.Procedure(ctx, itemSchema, ItemProcedure{
OID: id.Procedure(internalID),
Item: &p,
})
return true, err
}
return false, nil
})
}

// runSequence is called by RunCallback to handle Section_Sequence.
func runSequence(ctx *sql.Context, internalID id.Id, callbacks Callbacks, itemSchema ItemSchema) error {
collection, err := core.GetSequencesCollectionFromContext(ctx, itemSchema.Item.Name())
Expand Down Expand Up @@ -756,7 +903,8 @@ func runView(ctx *sql.Context, internalID id.Id, callbacks Callbacks, itemSchema
// runCallbackValidation ensures that the callbacks match the given oid.
func runCallbackValidation(ctx *sql.Context, internalID id.Id, callbacks Callbacks) bool {
// Check that we have the relevant callback, and return early if we do not
switch internalID.Section() {
s := internalID.Section()
switch s {
case id.Section_Check:
if callbacks.Check == nil {
return false
Expand All @@ -772,6 +920,10 @@ func runCallbackValidation(ctx *sql.Context, internalID id.Id, callbacks Callbac
if callbacks.ForeignKey == nil {
return false
}
case id.Section_Function:
if callbacks.Function == nil {
return false
}
case id.Section_Index:
if callbacks.Index == nil {
return false
Expand All @@ -780,6 +932,10 @@ func runCallbackValidation(ctx *sql.Context, internalID id.Id, callbacks Callbac
if callbacks.Schema == nil {
return false
}
case id.Section_Procedure:
if callbacks.Procedure == nil {
return false
}
case id.Section_Sequence:
if callbacks.Sequence == nil {
return false
Expand All @@ -803,7 +959,9 @@ func (iter Callbacks) iteratesOverSchemas() bool {
return iter.Check != nil ||
iter.ColumnDefault != nil ||
iter.ForeignKey != nil ||
iter.Function != nil ||
iter.Index != nil ||
iter.Procedure != nil ||
iter.Schema != nil ||
iter.Sequence != nil ||
iter.Table != nil ||
Expand Down
20 changes: 18 additions & 2 deletions server/functions/pg_get_functiondef.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package functions
import (
"github.com/dolthub/go-mysql-server/sql"

"github.com/dolthub/doltgresql/core/id"

"github.com/dolthub/doltgresql/server/functions/framework"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
Expand All @@ -34,7 +36,21 @@ var pg_get_functiondef_oid = framework.Function1{
IsNonDeterministic: true,
Strict: true,
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

High severity Low-privilege routine source disclosure

What failed: The expected visibility boundary is inconsistent: pg_proc enforces permission checks, but pg_get_functiondef bypasses that boundary and returns full routine definitions to low-privilege users.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: High High severity
  • Impact: Low-privilege users can read full function and procedure definitions they should not be able to access, exposing protected database logic and potentially sensitive implementation details.
  • Steps to Reproduce:
    1. Create a low-privilege login role with CONNECT on the database and USAGE on schema public.
    2. Revoke SELECT on pg_catalog.pg_proc from that role and confirm direct pg_proc reads are denied.
    3. As the same role, call pg_get_functiondef with known routine OIDs for a function and a procedure.
    4. Observe that full CREATE FUNCTION/CREATE PROCEDURE definitions are returned instead of an authorization error or redacted result.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In server/functions/pg_get_functiondef.go, the PR replaced the previous stub with direct lookup logic that calls RunCallback and copies function/procedure Item.Definition into the response. In server/functions/iterate.go, RunCallback -> runFunction/runProcedure iterates catalog collections and returns matched objects without any privilege check for the caller before invoking callbacks. Together, this creates a direct metadata disclosure path independent of pg_proc table permissions. The smallest practical fix is to add an explicit authorization check in pg_get_functiondef (or inside RunCallback for function/procedure sections) and return an error or empty result when the caller lacks routine-definition visibility.
  • Why this is likely a bug: The runtime evidence shows permission denial on pg_proc but successful routine-definition retrieval through pg_get_functiondef for the same low-privilege role, and the code path confirms no authorization gate before returning definition text. That mismatch indicates a real product authorization defect rather than test setup noise.
Relevant code

server/functions/pg_get_functiondef.go:38-54

Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
	oidVal := val.(id.Id)
	result := ""
	err := RunCallback(ctx, oidVal, Callbacks{
		Function: func(ctx *sql.Context, schema ItemSchema, function ItemFunction) (cont bool, err error) {
			result = function.Item.Definition
			return false, nil
		},
		Procedure: func(ctx *sql.Context, schema ItemSchema, procedure ItemProcedure) (cont bool, err error) {
			result = procedure.Item.Definition
			return false, nil
		},
	})
	if err != nil {
		return "", err
	}
	return result, nil
},

server/functions/iterate.go:824-838

func runProcedure(ctx *sql.Context, internalID id.Id, callbacks Callbacks, itemSchema ItemSchema) error {
	collection, err := core.GetProceduresCollectionFromContext(ctx, itemSchema.Item.Name())
	if err != nil {
		return err
	}
	return collection.IterateProcedures(ctx, func(p procedures.Procedure) (stop bool, err error) {
		if p.ID.AsId() == internalID {
			_, err = callbacks.Procedure(ctx, itemSchema, ItemProcedure{
				OID:  id.Procedure(internalID),
				Item: &p,
			})
			return true, err
		}
		return false, nil
	})
}
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**High severity — Low-privilege routine source disclosure**

**What failed:** The expected visibility boundary is inconsistent: pg_proc enforces permission checks, but pg_get_functiondef bypasses that boundary and returns full routine definitions to low-privilege users.

- **Impact:** Low-privilege users can read full function and procedure definitions they should not be able to access, exposing protected database logic and potentially sensitive implementation details.
- **Steps to reproduce:**
  1. Create a low-privilege login role with CONNECT on the database and USAGE on schema public.
  2. Revoke SELECT on pg_catalog.pg_proc from that role and confirm direct pg_proc reads are denied.
  3. As the same role, call pg_get_functiondef with known routine OIDs for a function and a procedure.
  4. Observe that full CREATE FUNCTION/CREATE PROCEDURE definitions are returned instead of an authorization error or redacted result.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** In server/functions/pg_get_functiondef.go, the PR replaced the previous stub with direct lookup logic that calls RunCallback and copies function/procedure Item.Definition into the response. In server/functions/iterate.go, RunCallback -> runFunction/runProcedure iterates catalog collections and returns matched objects without any privilege check for the caller before invoking callbacks. Together, this creates a direct metadata disclosure path independent of pg_proc table permissions. The smallest practical fix is to add an explicit authorization check in pg_get_functiondef (or inside RunCallback for function/procedure sections) and return an error or empty result when the caller lacks routine-definition visibility.
- **Why this is likely a bug:** The runtime evidence shows permission denial on pg_proc but successful routine-definition retrieval through pg_get_functiondef for the same low-privilege role, and the code path confirms no authorization gate before returning definition text. That mismatch indicates a real product authorization defect rather than test setup noise.

**Relevant code:**

`server/functions/pg_get_functiondef.go:38-54`

~~~go
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
	oidVal := val.(id.Id)
	result := ""
	err := RunCallback(ctx, oidVal, Callbacks{
		Function: func(ctx *sql.Context, schema ItemSchema, function ItemFunction) (cont bool, err error) {
			result = function.Item.Definition
			return false, nil
		},
		Procedure: func(ctx *sql.Context, schema ItemSchema, procedure ItemProcedure) (cont bool, err error) {
			result = procedure.Item.Definition
			return false, nil
		},
	})
	if err != nil {
		return "", err
	}
	return result, nil
},
~~~

`server/functions/iterate.go:824-838`

~~~go
func runProcedure(ctx *sql.Context, internalID id.Id, callbacks Callbacks, itemSchema ItemSchema) error {
	collection, err := core.GetProceduresCollectionFromContext(ctx, itemSchema.Item.Name())
	if err != nil {
		return err
	}
	return collection.IterateProcedures(ctx, func(p procedures.Procedure) (stop bool, err error) {
		if p.ID.AsId() == internalID {
			_, err = callbacks.Procedure(ctx, itemSchema, ItemProcedure{
				OID:  id.Procedure(internalID),
				Item: &p,
			})
			return true, err
		}
		return false, nil
	})
}
~~~

// TODO: real implementation
return "", nil
oidVal := val.(id.Id)
result := ""
err := RunCallback(ctx, oidVal, Callbacks{
Function: func(ctx *sql.Context, schema ItemSchema, function ItemFunction) (cont bool, err error) {
result = function.Item.Definition
return false, nil
},
Procedure: func(ctx *sql.Context, schema ItemSchema, procedure ItemProcedure) (cont bool, err error) {
result = procedure.Item.Definition
return false, nil
},
})
if err != nil {
return "", err
}
return result, nil
},
}
3 changes: 3 additions & 0 deletions server/tables/pgcatalog/pg_catalog_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type pgCatalogCache struct {
// pg_index / pg_indexes
pgIndexes *pgIndexCache

// pg_proc
procs []*pgProc

// pg_sequence / pg_sequences
sequences []*pgSequence

Expand Down
Loading
Loading