Skip to content

Conversation

@x032205
Copy link
Contributor

@x032205 x032205 commented Nov 19, 2025

Read-only toggle for PAM DB

@x032205 x032205 requested a review from fangpenlin November 19, 2025 04:05
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 19, 2025

Greptile Summary

  • Implements read-only mode toggle for PAM database sessions by adding ReadOnlyMode boolean field propagated through credentials API to proxy handlers
  • Uses dual-layer protection: database-level session settings (SET SESSION TRANSACTION READ ONLY for MySQL, SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY for PostgreSQL) plus application-level query filtering via sql-query-identifier library
  • Query filtering checks all SQL statements and blocks operations not classified as ExecutionListing or ExecutionInformation types

Confidence Score: 2/5

  • This PR has critical security vulnerabilities in the read-only enforcement mechanism
  • The implementation relies on an external SQL parser library (sql-query-identifier) as a security boundary, which can be bypassed through parser bugs, unrecognized SQL syntax, stored procedures, or transaction control commands. The database-level session settings are the correct approach, but they use transaction-level defaults that can be overridden rather than session-level enforcement. Multiple attack vectors exist where write operations could bypass detection.
  • Pay critical attention to packages/pam/handlers/mysql/relay_handler.go and packages/pam/handlers/postgres.go - the query filtering logic creates a false sense of security and should either be removed or demoted to logging-only, with database-level permissions being the primary enforcement mechanism

Important Files Changed

Filename Overview
packages/pam/handlers/mysql/relay_handler.go Implements read-only query filtering for MySQL using sql-query-identifier library to block write operations
packages/pam/handlers/postgres.go Implements read-only query filtering for PostgreSQL using sql-query-identifier library to block write operations
packages/pam/handlers/mysql/proxy.go Adds ReadOnlyMode field and sets MySQL session to read-only using SET SESSION TRANSACTION READ ONLY
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile
React with 👍 or 👎 to share your feedback on this new summary format

Comment on lines +38 to +46
if r.config.ReadOnlyMode {
isReadOnly, err := r.handleReadOnlyCheck(query)
if err != nil {
return nil, err
}
if !isReadOnly {
return nil, mysql.NewError(mysql.ER_OPTION_PREVENTS_STATEMENT, "Operation not allowed by policy in read-only mode.")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like repeating, extracted into some thing like

err = checkReadOnly(...)
if err != nil {
    return ...
}

instead?

Comment on lines +151 to +174
dialect := sqi.DialectMySQL
strict := false
options := sqi.IdentifyOptions{
Dialect: &dialect,
Strict: &strict,
}

identifiedQueries, err := sqi.Identify(query, options)
if err != nil {
log.Error().
Str("sessionID", r.config.SessionID).
Str("query", query).
Err(err).
Msg("Failed to identify query; blocking in read-only mode.")
return false, err
}

// verify that every statement in the query is read-only
for _, identifiedQuery := range identifiedQueries {
if identifiedQuery.ExecutionType != sqi.ExecutionListing && identifiedQuery.ExecutionType != sqi.ExecutionInformation {
log.Warn().
Str("sessionID", r.config.SessionID).
Str("query", query).
Str("executionType", string(identifiedQuery.ExecutionType)).
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be automatic unit test tests covering different SQL statements trying to see if it can parse them correctly and detect write syntax. One test case for mysql and another for postgresql.

Comment on lines +786 to +804
for _, identifiedQuery := range identifiedQueries {
if identifiedQuery.ExecutionType != sqi.ExecutionListing && identifiedQuery.ExecutionType != sqi.ExecutionInformation {
log.Warn().
Str("sessionID", p.config.SessionID).
Str("query", queryContent).
Str("executionType", string(identifiedQuery.ExecutionType)).
Msg("Write query blocked in read-only mode.")

errorResponse := &pgproto3.ErrorResponse{
Severity: "ERROR",
Code: "42803", // insufficient_privilege
Message: "Operation not allowed by policy in read-only mode.",
}
clientBackend.Send(errorResponse)
_ = clientBackend.Flush()
errChan <- fmt.Errorf("write query blocked: %s (type: %s)", queryContent, identifiedQuery.ExecutionType)
return false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

unit test cases

Comment on lines +73 to +81
if r.config.ReadOnlyMode {
isReadOnly, err := r.handleReadOnlyCheck(query)
if err != nil {
return 0, 0, nil, err
}
if !isReadOnly {
return 0, 0, nil, mysql.NewError(mysql.ER_OPTION_PREVENTS_STATEMENT, "Operation not allowed by policy in read-only mode.")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

DRY

Comment on lines +91 to +99
if r.config.ReadOnlyMode {
isReadOnly, err := r.handleReadOnlyCheck(query)
if err != nil {
return nil, err
}
if !isReadOnly {
return nil, mysql.NewError(mysql.ER_OPTION_PREVENTS_STATEMENT, "Operation not allowed by policy in read-only mode.")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

DRY

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants