Secure Go Code using the Principle of Least Privilege

Principle of Least Privilege in Software Engineering means granting only the bare minimum permissions necessary for a process/module/user or any other entity to perform the required task. For example in Kubernetes, assigning only the necessary minimum privileges to a pod’s service account to perform it’s functions. If a Pod needs only read access to a specific config map in a namespace, then following the principle of least privilege means creating a role and rolebinding on that namespace with access to only that specific config map and not providing any other permissions. Following the principle of least privilege will reduce the attack surface in case a cyber attack happens.

This Principle of Least Privilege can also be followed when writing Go code. Let’s talk about the different scenarios where this principle can be applied.

Over privileged DB handles

Giving access to *sql.DB handle to http handlers will give them privileges to perform any operation on the entire database. Consider the following http handler that only needs to retrieve a user’s profile.

1func ProfileHandler(db *sql.DB) http.HandlerFunc {
2    return func(w http.ResponseWriter, r *http.Request) {
3        userID := r.URL.Query().Get("id")
4        row := db.QueryRow("SELECT username, bio FROM users WHERE id = ?", userID)
5    }
6}

The handler only needs one database operation, read a profile. However, by passing *sql.DB, we give the handler a much broader capability. The handler can now execute any operation exposed by *sql.DB:

1db.Query(...)       // Read arbitrary data
2db.Exec(...)        // Modify/delete data
3db.Begin(...)       // Start transactions

and so on.

For example, there is nothing preventing someone from accidentally adding the code to delete users to the handler the handler.

 1func ProfileHandler(db *sql.DB) http.HandlerFunc {
 2    return func(w http.ResponseWriter, r *http.Request) {
 3
 4        // Intended operation
 5        userID := r.URL.Query().Get("id")
 6        row := db.QueryRow("SELECT username, bio FROM users WHERE id = ?", userID)
 7
 8        // Unrelated capability that this handler should never need
 9        _, err := db.Exec("DELETE FROM users WHERE id = ?", userID)
10
11    }
12}

Line no. 9 of the above function contains code to delete users from the DB!

Fix for Over privileged DB handles

The handler should have access only to the specific operations it needs to perform on the database. In the ProfileHandler case, it only needs access to read the user data from the user table.

The fix is to define an interface with only the capabilities needed for the ProfileHandler and pass it to the profile handler.

 1type ProfileStore interface {
 2    GetPublicProfile(id string) (username string, bio string, err error)
 3}
 4
 5type Profile struct {
 6	db *sql.DB
 7}
 8
 9func (p Profile) GetPublicProfile(id string) (username string, bio string, err error) {
10	row := p.db.QueryRow("SELECT username, bio FROM users WHERE id = ?", userID)
11	...
12	return username, bio, nil
13}
14
15func NewProfileStore(db *sql.DB) ProfileStore {
16	return Profile {
17		db: db,
18	}
19}
20
21// The ProfileHandler handler uses the ProfileStore interface
22func ProfileHandler(store ProfileStore) http.HandlerFunc {
23    return func(w http.ResponseWriter, r *http.Request) {
24        userID := r.URL.Query().Get("id")    
25
26        username, bio, err := store.GetPublicProfile(userID)
27    }
28}

The handler now doesn’t have access to the entire database. It can only read profile data and cannot perform any other action on the db.

Pushing the principle of least privilege to the DB layer

While the above fix alleviates the issue of the over privileged http handler in the application layer, the code can be made more secure by pushing the security to the database layer.

A user can be provisioned with read only access to the users table in postgres and that user can be used in the Go code to create the profile store used by the http handler.

A profile_reader user can be created in postgres with read only access to the user table. This can be done using the following query.

1GRANT SELECT ON TABLE public.users TO profile_reader;

The above profile_reader user only has read only access to the user table and it can be used to create the ProfileStore needed by the ProfileHandler.

 1type ProfileStore interface {
 2    GetPublicProfile(id string) (username string, bio string, err error)
 3}
 4
 5type Profile struct {
 6	db *sql.DB
 7}
 8
 9func (p Profile) GetPublicProfile(id string) (username string, bio string, err error) {
10	//implementation
11}
12
13func NewProfileStore(db *sql.DB) ProfileStore {
14	return Profile {
15		db: db,
16	}
17}
18
19func ProfileHandler(store ProfileStore) http.HandlerFunc {
20    return func(w http.ResponseWriter, r *http.Request) {
21        userID := r.URL.Query().Get("id")    
22
23        username, bio, err := store.GetPublicProfile(userID)
24    }
25}
26
27func main() {
28    dsn := "postgres://profile_reader:secure_password@127.0.0.1:5432/my_app_db"
29    userTable, err := sql.Open("mysql", dsn)
30    if err != nil {
31        log.Fatal(err)
32    }
33
34    profileStore := NewProfileStore(userTable)
35    handler := ProfileHandler(profileStore)
36}

In line no. 28 of the above code, the profile_reader user which has only read access on the users table is used to create the profileStore.

Using bidirectional channels when unidirectional is sufficient

A consumer in a worker pool receives a channel which can both read from and written to. There could be a code bug which causes the consumer to write jobs back into the channel flooding the worker pool.

 1func consumer(id int, jobCh chan Job) {
 2    for {
 3        job := <-jobCh
 4		...
 5
 6        // The consumer accidentally writes a job back into the channel.
 7        jobCh <- Job{ID: 0} 
 8        }
 9    }
10}

Fix for above code - Use unidirectional channels

jobCh in the below code is now a unidirectional channel which can only be read from and cannot be written to.

1func consumer(id int, jobCh <-chan Job) {
2    for {
3        job := <-jobCh
4		...
5        // This line now causes a compile time error since the channel is unidirectional 
6        jobCh <- Job{ID: 0} 
7        }
8    }
9}

When the consumer tries to write to the jobCh, there will be a compilation error invalid operation: cannot send to receive-only channel <-chan int jobCh (variable of type <-chan int)

Inadvertently logging sensitive data

Logging using the %+v format specifier will lead to inadvertent leak of sensitive and Personally Identifiable Information(PII). Let’s say the code has the following order struct which holds order information.

1order := Order{
2    id:     "1",
3    amount: 6.6,
4    customerID: "abc"
5}
6
7log.Printf("Processing order %+v", o)

%+v will log all the fields in the struct.

If a developer at a later point in time adds customer email and shipping address fields to this struct, the code will now log PII like the customer email and shipping address.

1order := Order{
2    id:     "1",
3    amount: 6.6,
4    customerID: "abc",
5    shippingAddress: "UK",
6    customerEmail: "some@example.com"
7}
8
9log.Printf("Processing order %+v", order) //This will now log PII

Fix for logging vulnerability - Log only necessary fields

Don’t use the "%+v" format specifier while logging and only log the necessary fields

1order := Order{
2    id:     "1",
3    amount: 6.6,
4    customerID: "abc",
5    shippingAddress: "UK",
6    customerEmail: "some@example.com"
7}
8
9log.Printf("Processing order %s %f", order.id, order.amount) //This will not leak PII

Unmarshalling user input using over privileged structs

The following struct represents user data.

1type User struct {
2    ID       int    `json:"id"`
3    Email    string `json:"email"`
4    Password string `json:"password"` 
5    IsAdmin  bool   `json:"is_admin"`
6}

The following function takes a json http request body and unmarshals it using the User struct. An attacker can set the IsAdmin field in the json which will make him an admin!

1func CreateUserFromRequest(body []byte) (*User, error) {
2    var user User
3    if err := json.Unmarshal(body, &user); err != nil {
4        return nil, err
5    }
6    
7    return &user, nil
8}

An attacker Attacker can set the IsAdmin field by sending the following json request

1{"id":1, "email":"hacker@example.com","password":"123","is_admin":true}

Fix for above vulnerability - Use a separate struct for parsing user input

 1// Struct to process user input
 2type UserRequest struct {
 3    Email    string `json:"email"`
 4    Password string `json:"password"`
 5}
 6
 7// Struct for internal representation
 8type User struct {
 9    ID        int
10    Email     string
11    Password  string
12    IsAdmin   bool
13    CreatedAt time.Time
14}
15
16
17func CreateUserFromRequest(body []byte) (*User, error) {
18    var userRequest UserRequest
19    if err := json.Unmarshal(body, &userRequest); err != nil {
20        return nil, err
21    }   
22
23    // Create internal user using the information from the `userRequest` struct
24    user := User {
25        ID:        generateID(),
26        Email:     userRequest.Email,
27        Password:  hashPassword(userRequest.Password), 
28        IsAdmin:   false,
29    }
30}

The CreateUserFromRequest function now uses the UserRequest struct to unmarshal user input which prevents any internal fields from being exposed to the public.

I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter or LinkedIn. Have a good day.

If you are looking for a simple tool to check ssl expiry, visit my website https://sslnotify.com/.