Using reflection in Golang to scrub sensitive data from logs
Logging sensitive data is an enduring issue that has resulted in some famous CVEs, such as CVE-2020-2004 and CVE-2014-0920. Scrubbing such data before logging it is a problem that every development team must solve at some point.
In this blog post, we’ll discuss a solution used at Nutanix to find and scrub sensitive data in Golang structures. Golang is widely used in the Nutanix tech stack, with usage ranging from the gateway, infrastructure, control, and data path codebases.
Reflection: Interacting with a struct at runtime
Since the scrubbing utility function needs to work on any Golang struct where its fields and members are known only at runtime, we can leverage reflect, a powerful package from the Golang standard library.
For the purpose of this blog post, let’s write a function Scrub()
, which takes a pointer to a struct and a set of field names to scrub. Our function Scrub()
returns a JSON-marshaled string representation of the scrubbed struct, which is suitable for logging by the caller function.
The overall function signature can be described as follows:
// Scrub redacts all the specified string fields in the 'input' struct
// at any level recursively and returns a JSON-formatted string of the scrubbed struct.
func Scrub(input interface{}, fieldsToRedact map[string]bool) string {
// 1. Call a recursive function to find and scrub fields in input at any level.
// 2. Create a JSON-marshaled string from the scrubbed string.
// 3. Return the scrubbed string.
}
Taking a deep dive into reflection
Reflection acts on three important reflection properties that every Golang object has: Type, Kind, and Value. ‘Kind’ can be one of struct, int, string, slice, map, or one of the other Golang primitives.
The reflect package provides the following functions to get these properties:
// Get the reflection value of an object.
rValue := reflect.ValueOf(input)
// Get the reflection kind of an object.
rKind := rValue.Kind()
// Get the reflection type of an object (two options).
rType := reflect.TypeOf(input)
rType := rValue.Type()
// Get the underlying object of a pointer object.
underlyingValue := rValue.Elem()
// Traverse through all the fields of a struct.
if rType.Kind() == reflect.Struct {
for i := 0; i < rType.NumField(); i++ {
fieldValue := rValue.Field(i)
}
}
The reflect package also allows you to modify the value of a specific field given the reflected value of a field. In practice, this feature can be used to scrub the value of a specific string field to “*******” and hide a customer password from prying eyes!
// Redact this string value. Other types are not redacted.
if fieldValue.CanSet() && fieldValue.Kind() == reflect.String && !fieldValue.IsZero() {
fieldValue.SetString("********")
}
Because a struct field can be an array of strings, each containing some sensitive data, it’s important to loop over all the elements of an array for scrubbing.
if rType.Kind() == reflect.Array || rType.Kind() == reflect.Slice {
for i := 0; i < rValue.Len(); i++ {
arrValue := rValue.Index(i)
// Scrub if the arrValue is of type string, as described above.
}
}
Because a nested struct can be a field of a parent struct, we need to recurse over the internal struct field to scrub all sensitive data inside it.
if rType.Kind() == reflect.Struct {
for i := 0; i < rType.NumField(); i++ {
fieldValue := rValue.Field(i)
// Recurse on fieldValue to scrub its fields.
}
}
Some important caveats
Reflection can modify a field of a struct only if the struct is passed by pointer, and only if the field is exported (public). Exported fields in Golang begin with a Unicode uppercase letter. You can use the reflect package to check for a pointer by calling the function rValue.CanAddr()
. Additionally, you can check for an exported field by calling rValue.Addr().CanInterface()
.
Similarly, reflection cannot act on zero-value fields. Such values can be skipped by calling the rValue.IsValid()
function.
It may be important to restore the original values in the struct before returning to the caller. To do so, a slice can be passed to save the original values. Then, the same recursive call can be used to restore the original value of each modified field. This works because the order of traversing in a specific struct is always the same.
A boolean argument mask can be used to control the scrubbing behavior. One can invoke Scrub()
with mask=true
to scrub the fields of a structure. A subsequent call to Scrub()
can be invoked with mask=false
to restore the original values.
func Scrub(input interface{}, fieldsToRedact map[string]bool) string {
// 1. Call a recursive function to find and scrub fields in input at any level.
savedValues := make([]string, 0)
ScrubRecursive(input, "", fieldsToRedact, &savedValues, true /* mask */)
// 2. Get a JSON-marshaled string from the scrubbed struct.
var b []byte
b, _ = json.Marshal(input)
// Restore all the scrubbed values back to the original values in the struct.
ScrubRecursive(input, "", fieldsToRedact, &savedValues, false /* unmask */)
// Return the scrubbed string.
return string(b)
}
Now, the caller can pass any kind of struct by pointer to Scrub()
with the desired fields to scrub (such as ‘password’, ‘secret’, and ‘key’) and get a scrubbed string to log safely without exposing any sensitive data.
Example
With all the code in place for our function Scrub()
, we’ll use the example input struct below:
users := &Users{
Secret: "secret_sshhh",
Keys: []string{"key_1", "key_2", "key_3"},
UserInfo: []User{
{
Username: "John Doe",
Password: "John_Doe's_Password",
DbSecrets: []string{"secret_1", "secret_2"},
},
{
Username: "Jane Doe",
Password: "Jane_Doe's_Password",
DbSecrets: []string{"secret_3", "secret_4"},
},
},
}
Calling Scrub()
with fields {‘Secret’, ‘Keys’, ‘DbSecrets’, ‘Password’} modifies the struct as follows before converting it to a JSON-marshaled string for logging:
users := &Users{
Secret: "********",
Keys: []string{"********", "********", "********"},
UserInfo: []User{
{
Username: "John Doe",
Password: "********",
DbSecrets: []string{"********", "********"},
},
{
Username: "Jane Doe",
Password: "********",
DbSecrets: []string{"********", "********"},
},
},
}
Full Code
The full utility with working code is available at https://github.com/ssrathi/go-scrub/.
Conclusion
Reflection is a powerful tool, and this blog post only scratches the surface of what it can do! At Nutanix, studying and using the Golang reflect package is just one of the ways we improve security and scalability for our 20,000+ customers.