Skip to content

Commit b9fcb19

Browse files
authored
redact: package for redacting error messages (#841)
Package redact implements functions to redact sensitive information from errors. A basic example is: name := "Alex" id := 5 err := redact.Errorf("error getting user %s with ID %d", name, Safe(id)) fmt.Println(err) // error getting user Alex with ID 5 fmt.Println(redact.Error(err)) // error getting user <redacted string> with ID 5 See the package docs and tests for more examples. This will allow us to get more information in Sentry without sending any identifying information.
1 parent 7e4abe7 commit b9fcb19

File tree

4 files changed

+550
-21
lines changed

4 files changed

+550
-21
lines changed

.golangci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ linters-settings:
4040
varnamelen:
4141
max-distance: 10
4242
ignore-decls:
43+
- a []any
4344
- c echo.Context
4445
- const C
4546
- e error
4647
- e watch.Event
4748
- f *foo.Bar
49+
- f fmt.State
4850
- i int
4951
- id string
5052
- m map[string]any
@@ -54,6 +56,7 @@ linters-settings:
5456
- r *http.Request
5557
- r io.Reader
5658
- r *os.File
59+
- re *regexp.Regexp
5760
- sh *Shell
5861
- sh *shell
5962
- sh *shell.Shell

internal/debug/debug.go

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"log"
77
"os"
8+
"runtime"
89
"strconv"
910

1011
"github.com/getsentry/sentry-go"
@@ -51,29 +52,18 @@ func Recover() {
5152
fmt.Println("Error:", r)
5253
}
5354

54-
func EarliestStackTrace(err error) errors.StackTrace {
55-
type stackTracer interface {
56-
StackTrace() errors.StackTrace
57-
}
58-
59-
type causer interface {
60-
Cause() error
61-
}
62-
63-
var st stackTracer
64-
var earliestStackTrace errors.StackTrace
55+
func EarliestStackTrace(err error) error {
56+
type pkgErrorsStackTracer interface{ StackTrace() errors.StackTrace }
57+
type redactStackTracer interface{ StackTrace() []runtime.Frame }
6558

59+
var stErr error
6660
for err != nil {
67-
if errors.As(err, &st) {
68-
earliestStackTrace = st.StackTrace()
61+
//nolint:errorlint
62+
switch err.(type) {
63+
case redactStackTracer, pkgErrorsStackTracer:
64+
stErr = err
6965
}
70-
71-
var c causer
72-
if !errors.As(err, &c) {
73-
break
74-
}
75-
err = c.Cause()
66+
err = errors.Unwrap(err)
7667
}
77-
78-
return earliestStackTrace
68+
return stErr
7969
}

internal/redact/redact.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Package redact implements functions to redact sensitive information from
2+
// errors.
3+
//
4+
// Redacting an error replaces its message with a placeholder while still
5+
// maintaining wrapped errors:
6+
//
7+
// wrapped := errors.New("not found")
8+
// name := "Alex"
9+
// err := fmt.Errorf("error getting user %s: %w", name, wrapped)
10+
//
11+
// fmt.Println(err)
12+
// // error getting user Alex: not found
13+
//
14+
// fmt.Println(Error(err))
15+
// // <redacted *fmt.wrapError>: <redacted *errors.errorString>
16+
//
17+
// If an error implements a Redact() string method, then it is said to be
18+
// redactable. A redactable error defines an alternative message for its
19+
// redacted form:
20+
//
21+
// type userErr struct{ name string }
22+
//
23+
// func (e *userErr) Error() string {
24+
// return fmt.Sprintf("user %s not found", e.name)
25+
// }
26+
//
27+
// func (e *userErr) Redact() string {
28+
// return fmt.Sprintf("user %x not found", sha256.Sum256([]byte(e.name)))
29+
// }
30+
//
31+
// func main() {
32+
// err := &userErr{name: "Alex"}
33+
// fmt.Println(err)
34+
// // user Alex not found
35+
//
36+
// fmt.Println(Error(err))
37+
// // user db74c940d447e877d119df613edd2700c4a84cd1cf08beb7cbc319bcfaeab97a not found
38+
// }
39+
//
40+
// The [Errorf] function creates redactable errors that retain their literal
41+
// format text, but redact any arguments. The format string spec is identical
42+
// to that of [fmt.Errorf]. Calling [Safe] on an [Errorf] argument will include
43+
// it in the redacted message.
44+
//
45+
// name := "Alex"
46+
// id := 5
47+
// err := Errorf("error getting user %s with ID %d", name, Safe(id))
48+
//
49+
// fmt.Println(err)
50+
// // error getting user Alex with ID 5
51+
//
52+
// fmt.Println(Error(err))
53+
// // error getting user <redacted string> with ID 5
54+
//
55+
//nolint:errorlint
56+
package redact
57+
58+
import (
59+
"errors"
60+
"fmt"
61+
"runtime"
62+
)
63+
64+
// Error returns a redacted error that wraps err. If err has a Redact() string
65+
// method, then Error uses it for the redacted error message. Otherwise, Error
66+
// recursively redacts each wrapped error, joining them with ": " to create the
67+
// final error message. If it encounters an error that has a Redact() method,
68+
// then it appends the result of Redact() to the message and stops unwrapping.
69+
func Error(err error) error {
70+
if err == nil {
71+
return nil
72+
}
73+
74+
switch t := err.(type) {
75+
case *redactedError:
76+
// Don't redact an already redacted error, otherwise its redacted message
77+
// will be replaced with a placeholder.
78+
return err
79+
case redactor:
80+
return &redactedError{
81+
msg: t.Redact(),
82+
wrapped: err,
83+
}
84+
default:
85+
msg := placeholder(err)
86+
wrapped := err
87+
for {
88+
wrapped = errors.Unwrap(wrapped)
89+
if wrapped == nil {
90+
break
91+
}
92+
if redactor, ok := wrapped.(redactor); ok {
93+
msg += ": " + redactor.Redact()
94+
break
95+
}
96+
msg += ": " + placeholder(wrapped)
97+
}
98+
return &redactedError{
99+
msg: msg,
100+
wrapped: err,
101+
}
102+
}
103+
}
104+
105+
// Errorf creates a redactable error that has an error string identical to that
106+
// of a [fmt.Errorf] error. Calling [Redact] on the result will redact all
107+
// format arguments from the error message instead of redacting the entire
108+
// string.
109+
//
110+
// When redacting the error string, Errorf replaces arguments that implement a
111+
// Redact() string method with the result of that method. To include an
112+
// argument as-is in the redacted error, first call [Safe]. For example:
113+
//
114+
// username := "bob"
115+
// Errorf("cannot find user %s", username).Error()
116+
// // cannot find user <redacted string>
117+
//
118+
// Errorf("cannot find user %s", Safe(username)).Error()
119+
// // cannot find user bob
120+
func Errorf(format string, a ...any) error {
121+
// Capture a stack trace.
122+
safeErr := &safeError{
123+
callers: make([]uintptr, 32),
124+
}
125+
n := runtime.Callers(2, safeErr.callers)
126+
safeErr.callers = safeErr.callers[:n]
127+
128+
// Create the "normal" unredacted error. We need to remove the safe wrapper
129+
// from any args so that fmt.Errorf can detect and format their type
130+
// correctly.
131+
args := make([]any, len(a))
132+
for i := range a {
133+
if safe, ok := a[i].(safe); ok {
134+
args[i] = safe.a
135+
} else {
136+
args[i] = a[i]
137+
}
138+
}
139+
safeErr.err = fmt.Errorf(format, args...)
140+
141+
// Now create the redacted error by replacing all args with their redacted
142+
// version or by inserting a placeholder if the arg can't be redacted.
143+
for i := range a {
144+
switch t := a[i].(type) {
145+
case safe:
146+
args[i] = t.a
147+
case error:
148+
args[i] = Error(t)
149+
case redactor:
150+
args[i] = formatter(t.Redact())
151+
default:
152+
args[i] = formatter(placeholder(t))
153+
}
154+
}
155+
safeErr.redacted = fmt.Errorf(format, args...)
156+
return safeErr
157+
}
158+
159+
// redactor defines the Redact interface for types that can format themselves
160+
// in redacted errors.
161+
type redactor interface {
162+
Redact() string
163+
}
164+
165+
// safe wraps a value that is marked as safe for including in a redacted error.
166+
type safe struct{ a any }
167+
168+
// Safe marks a value as safe for including in a redacted error.
169+
func Safe(a any) any {
170+
return safe{a}
171+
}
172+
173+
// safeError is an error that can redact its message.
174+
type safeError struct {
175+
err error
176+
redacted error
177+
callers []uintptr
178+
}
179+
180+
func (e *safeError) Error() string { return e.err.Error() }
181+
func (e *safeError) Redact() string { return e.redacted.Error() }
182+
func (e *safeError) Unwrap() error { return e.err }
183+
184+
func (e *safeError) StackTrace() []runtime.Frame {
185+
if len(e.callers) == 0 {
186+
return nil
187+
}
188+
frameIter := runtime.CallersFrames(e.callers)
189+
frames := make([]runtime.Frame, 0, len(e.callers))
190+
for {
191+
frame, more := frameIter.Next()
192+
frames = append(frames, frame)
193+
if !more {
194+
break
195+
}
196+
}
197+
return frames
198+
}
199+
200+
func (e *safeError) Format(f fmt.State, verb rune) {
201+
switch verb {
202+
case 'v', 's':
203+
f.Write([]byte(e.Error()))
204+
if f.Flag('+') {
205+
for _, fr := range e.StackTrace() {
206+
fmt.Fprintf(f, "\n%s\n\t%s:%d", fr.Function, fr.File, fr.Line)
207+
}
208+
return
209+
}
210+
case 'q':
211+
fmt.Fprintf(f, "%q", e.Error())
212+
}
213+
}
214+
215+
// redactedError is an error containing a redacted message. It is usually the
216+
// result of calling Error(safeError).
217+
type redactedError struct {
218+
msg string
219+
wrapped error
220+
}
221+
222+
func (e *redactedError) Error() string { return e.msg }
223+
func (e *redactedError) Unwrap() error { return e.wrapped }
224+
225+
// formatter allows a string to be formatted by any fmt verb.
226+
// For example, fmt.Sprintf("%d", formatter("100")) will return "100" without
227+
// an error.
228+
type formatter string
229+
230+
func (f formatter) Format(s fmt.State, verb rune) {
231+
s.Write([]byte(f))
232+
}
233+
234+
// placeholder generates a placeholder string for values that don't satisfy
235+
// redactor.
236+
func placeholder(a any) string {
237+
return fmt.Sprintf("<redacted %T>", a)
238+
}

0 commit comments

Comments
 (0)