By default Go allows you to easily format, log or output values of most types.
For example, the following code prints the XYZ
struct as a string.
package main
import (
"fmt"
)
type MyData struct {
Field string
}
func main() {
d := MyData{
Field: "Hello world!",
}
fmt.Println(d)
}
Usually this is a good thing, as it allows for easy debugging and/or formatting.
However, when working with sensitive values there are situations in which this is an issue. Some values should never be exposed in any way or only in very specific situations.
Values like user submitted (plaintext) passwords, session identifiers or API tokens.
One stray logger.Debugf
, fmt.Println
or json.Marshal
in the wrong place and you could be leaking sensitive data to logs or worse.
Unexported fields are not enough
In the example below we pass a struct with an unexported field to a logger.
package main
import (
"fmt"
)
type MyData struct {
// the sensitive field is unexported
sensitive string
}
func main() {
d := MyData{
sensitive: "My secret data",
}
fmt.Println(d)
}
As you can see, the unexported field still got logged and exposed.
Unexported fields are still a good idea if you want to prevent other packages from accessing your sensitive values though.
We're not trying to completely prevent other packages from accessing values, only to protect ourselves from inadvertently leaking values.
Other packages can always use read from unexported fields using reflection
or the unsafe
package.
Preventing leakage
There are a handful of single-method interfaces that determine the way types are:
- Formatted by the
fmt
package. - Logged using
slog
. - Encoded to text or binary.
If you model your sensitive data as custom types, you can then implement these interfaces to prevent specific kinds of data leakage.
For the most sensitive values you want to ensure all of angles are covered. I link to an example that covers all angles at the end of the article.
fmt.Formatter
The fmt.Formatter
interface determines how the type is formatted for different verbs. In our case we probably want to overwrite it for all verbs.
type Formatter interface {
Format(f State, verb rune)
}
fmt.Stringer
and fmt.GoStringer
interfaces, however these only influence the %s
, %v
and %#v
formatting verbs.
Implementing and returning a hardcoded value (or the empty string) will make it impossible to be leaked by calls to fmt.Println
or the other formatting functions.
An example implementation could look like this:
package main
import (
"fmt"
"log/slog"
"os"
)
type MyData struct {
// the sensitive field is unexported
sensitive string
}
func (MyData) Format(f fmt.State, verb rune) {
f.Write([]byte("x"))
}
func main() {
d := MyData{
sensitive: "My secret data",
}
// does not print sensitive data.
fmt.Println(d)
// does not log sensitive data.
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("message", "data", d)
}
These functions are used by most logging packages, so you indirectly also prevent your values from being logged.
slog.Valuer
If you require the formatting functionality but want to hide your values from structured logs, you should implement the slog.Valuer
interface.
type LogValuer interface {
LogValue() Value
}
Again, returning a hard coded value or the empty string will make sure your sensitive values won’t be logged.
package main
import (
"fmt"
"log/slog"
"os"
)
type MyData struct {
// the sensitive field is unexported
sensitive string
}
func (t MyData) LogValue() slog.Value {
return slog.StringValue("x")
}
func main() {
d := MyData{
sensitive: "My secret data",
}
// prints sensitive data.
fmt.Println(d)
// does not log sensitive data.
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("message", "data", d)
}
encoding.TextMarshaler
Unexported fields will not be marshalled by the json
and xml
packages.
However, if you want to protect a non-struct type you will have to overwrite the default “marshaling to text” behaviour.
If you want the most bang for your buck, you should look at implementing the encoding.TextMarshaler
interface. This interface is used by both the json
and xml
packages when implemented on a type.
See the example below.
package main
import (
"encoding/json"
"fmt"
)
type Sensitive string
func (s Sensitive) MarshalText() ([]byte, error) {
return []byte("x"), nil
}
func main() {
s := Sensitive("My secret data")
b, err := json.Marshal(s)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}
Third party packages that deal with textual data should in theory also look for this type. Be sure to check that your documentation.
json.Marshaler
and/or xml.Marshaler
if you want different behaviour for different data types.
Secret marker
So what value should you return when sensitive data is formatted or logged by accident?
I’m in favor of returning a well known string, some kind of unique string that will stand out.
For example: <!SECRET_REDACTED!>
.
You can then set up an alert that will trigger when this string is encountered in your logs, allowing you to detect data leaks.
If I do this, I sometimes also implement the encoding.TextMarshaler
interface without it strictly being necessary to prevent data leakage.
Example: Password struct
In my example web application I use these solutions to prevent raw passwords from being logged by accident.
You can find the real code here.
Outro
This article showed how to prevent data leakage by implementing several Go interfaces from the standard library. I hope you learned something :)
If you have any questions or comments feel free to reach out.
Get my free newsletter every second week
Used by 500+ developers to boost their Go skills.
"I'll share tips, interesting links and new content. You'll also get a brief guide to time for developers for free."