Prevent sensitive data from leaking

Avatar of the author Willem Schots
28 Mar, 2024
~5 min.
RSS

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.

main.go
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.

main.go
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)
}

There are also the 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:

main.go
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.

main.go
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.

main.go
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.

Alternatively, you can implement the 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."

Avatar of the author
Willem Schots

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Bluesky, Twitter/X or LinkedIn.

Thanks for reading!