Ben E. C. Boyter's Blog

We don't need no stinking bastion host!
2023/12/10 (471 words)

So on occasion, I find myself wanting to run SQL against customer databases. Where this gets annoying is that because we are often in AWS using lambda I have no ability to connect to them. Occasionally im able to bully the friendly devops’ and security people to allow me access to a bastion host, which I can then connect to the SQL database.

However this is often a painful process if allowed at all, and so with permission I sometimes add a special route into the application allowing me to run arbitrary SQL as needed.

I thought this Go function worth keeping on my blog as something I can refer back to. Given a global database connection as DB function will take in any query, run it against the database, and return it in a formatted way which you can then display to the user. I tend to just return it as plain text from a protected route.

It’s not perfect, but good enough for those situations where you just want a small bit of information from the database. Note it has an explicit rollback to prevent any data loss, but I wouldn’t trust my life to that and you can of course still run queries that slow down the database. As such use with caution.

package data

import "database/sql"

// EvilSql run whatever you want... not to be used for anything really... its sql injectable, but useful
// since it rolls back the transaction at the end, its sort of limited in what can be done, so not quite as
// evil as you would expect
func EvilSql(s string) (string, error) {
 tx := DB.Begin()

 rows, err := tx.Raw(s).Rows()
 if err != nil {
  return "", err
 }

 // Get column names
 columns, err := rows.Columns()
 if err != nil {
  return "", err
 }

 // Make a slice for the values
 values := make([]sql.RawBytes, len(columns))

 // rows.Scan wants '[]interface{}' as an argument, so we must copy the
 // references into such a slice
 // See <http://code.google.com/p/go-wiki/wiki/InterfaceSlice> for details
 scanArgs := make([]interface{}, len(values))
 for i := range values {
  scanArgs[i] = &values[i]
 }

 data := ""

 // Fetch rows
 for rows.Next() {
  // get RawBytes from data
  err = rows.Scan(scanArgs...)
  if err != nil {
   return "", err
  }

  // Now do something with the data.
  // Here we just print each column as a string.
  var value string
  for i, col := range values {
   // Here we can check if the value is nil (NULL value)
   if col == nil {
    value = "NULL"
   } else {
    value = string(col)
   }
   data += columns[i] + ": " + value + "\n"
  }
  data += "-----------------------------------\n"
 }

 // remove this because if we don't commit we can leave it open for the most part
 tx.Rollback()

 return data, nil
}