I've written a simple go program using YAML and the MySQL drivers with the intention of providing a simple utility to update a database without exposing the username and password credentials to the user executing the program.
(I'm well aware that I could also write this in Python or some other scripting language and manage the permissions delegations using sudo but I'd like to try a different approach here, for my own edification).
After building the program I've used chgrp sys dbcreds.yaml && chmod 0640 dbcreds.yaml
and chgrp sys ./myprog && chmod g+s ./myprog
(as root) ... and everything seems to work. (I also tested that access was denied, as it should be, prior to the setGID step).
I also tested strace
and that results in permission denied (as it should be). (For fun I also ran ltrace -S
on it; this is under Linux. As expected I did not see many normal libc function calls ... through I am surprised to have seen a few pthread_....() and one malloc() calls in that listing. I guess the GO runtime does link to some system library functions after all).
My question: is this safe? Is there any known way to cause a Go program, such as this (below) to core dump or expose its memory after it has read these private credentials? Is there a way to drop my SGID privs after I've read my credentials? Are there any examples of SUID/SGID exploits on Go binaries? Is there a better way to do this? Is there a way to proactive prevent core dumps or ensure that sensitive data (credentials) would not be in core dumps?
One other note: I find the gopkg.in/yaml.v2 semantics to be a bit disconcerting. In my YAML file I have something like:
---
user me
pw mypassword
But in my code I have to use User and Pw (capitalized) rather than using lower case as I would have expected. I presume this is an implementation decision by the authors of Goyaml. Is that so?
#!go
package main
import (
"fmt"
"database/sql"
_ "github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"strconv"
)
type Creds struct {
User string
Pw string
}
func main() {
filename := "./dbcreds.yaml"
var creds Creds
conf, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
err = yaml.Unmarshal(conf, &creds)
if err != nil {
panic(err)
}
var arg1 int
arg1, err = strconv.Atoi(os.Args[1])
if err != nil {
panic(err.Error()) // Just for example purpose. You should use proper error handling instead of panic
}
fmt.Println("arg1: ", arg1, "\n")
dsn := fmt.Sprintf("%s:%s@/mydatabase", creds.User, creds.Pw)
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err.Error())
}
defer db.Close()
err = db.Ping()
if err != nil {
panic(err.Error())
}
stmtOut, err := db.Prepare("SELECT quant FROM c WHERE id >= ?")
if err != nil {
panic(err.Error())
}
defer stmtOut.Close()
rows, err := stmtOut.Query(arg1)
if err != nil {
panic(err.Error())
}
defer rows.Close()
for rows.Next() {
var quant int
err = rows.Scan(&quant)
if err != nil {
panic(err.Error())
}
fmt.Println(quant)
}
}