Skip to content

Commit fad1cec

Browse files
committed
elasticapm: sanitize cookies and POST form fields
Sanitize cookies and POST form fields by matching their names against a configurable regular expression. By default, we match against: (?i:password|passwd|pwd|secret|.*key|.*token|.*session.*|.*credit.*|.*card.*) The pattern can be overridden by specifying the environment variable ELASTIC_APM_SANITIZE_FIELD_NAMES. The value is treated as a regular expression, and is wrapped as "(?i:<pattern>)" to match case-insensitively by default. If a key should be matched case-sensitively, the flag can be unset with the syntax "(?-i:<pattern>)".
1 parent ace23e4 commit fad1cec

File tree

6 files changed

+281
-15
lines changed

6 files changed

+281
-15
lines changed

README.md

+27-15
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,33 @@ The two most critical configuration attributes are the server URL and
4444
the secret token. All other attributes have default values, and are not
4545
required to enable tracing.
4646

47-
Environment variable | Default | Description
48-
----------------------------------------|---------|------------------------------------------
49-
ELASTIC\_APM\_SERVER\_URL | | Base URL of the Elastic APM server. If unspecified, no tracing will take place.
50-
ELASTIC\_APM\_SECRET\_TOKEN | | The secret token for Elastic APM server.
51-
ELASTIC\_APM\_VERIFY\_SERVER\_CERT | true | Verify certificates when using https.
52-
ELASTIC\_APM\_FLUSH\_INTERVAL | 10s | Time to wait before sending transactions to the Elastic APM server. Transactions will be batched up and sent periodically.
53-
ELASTIC\_APM\_MAX\_QUEUE\_SIZE | 500 | Maximum number of transactions to queue before sending to the Elastic APM server. Once this number is reached, any new transactions will replace old ones until the queue is flushed.
54-
ELASTIC\_APM\_TRANSACTION\_MAX\_SPANS | 500 | Maximum number of spans to capture per transaction. After this is reached, new spans will not be created, and a dropped count will be incremented.
55-
ELASTIC\_APM\_TRANSACTION\_SAMPLE\_RATE | 1.0 | Number in the range 0.0-1.0 inclusive, controlling how many transactions should be sampled (i.e. include full detail.)
56-
ELASTIC\_APM\_ENVIRONMENT | | Environment name, e.g. "production".
57-
ELASTIC\_APM\_FRAMEWORK\_NAME | | Framework name, e.g. "gin".
58-
ELASTIC\_APM\_FRAMEWORK\_VERSION | | Framework version, e.g. "1.0".
59-
ELASTIC\_APM\_SERVICE\_NAME | | Service name, e.g. "my-service". If this is unspecified, the agent will report the program binary name as the service name.
60-
ELASTIC\_APM\_SERVICE\_VERSION | | Service version, e.g. "1.0".
61-
ELASTIC\_APM\_HOSTNAME | | Override for the hostname.
47+
Environment variable | Default | Description
48+
----------------------------------------|-----------|------------------------------------------
49+
ELASTIC\_APM\_SERVER\_URL | | Base URL of the Elastic APM server. If unspecified, no tracing will take place.
50+
ELASTIC\_APM\_SECRET\_TOKEN | | The secret token for Elastic APM server.
51+
ELASTIC\_APM\_VERIFY\_SERVER\_CERT | true | Verify certificates when using https.
52+
ELASTIC\_APM\_FLUSH\_INTERVAL | 10s | Time to wait before sending transactions to the Elastic APM server. Transactions will be batched up and sent periodically.
53+
ELASTIC\_APM\_MAX\_QUEUE\_SIZE | 500 | Maximum number of transactions to queue before sending to the Elastic APM server. Once this number is reached, any new transactions will replace old ones until the queue is flushed.
54+
ELASTIC\_APM\_TRANSACTION\_MAX\_SPANS | 500 | Maximum number of spans to capture per transaction. After this is reached, new spans will not be created, and a dropped count will be incremented.
55+
ELASTIC\_APM\_TRANSACTION\_SAMPLE\_RATE | 1.0 | Number in the range 0.0-1.0 inclusive, controlling how many transactions should be sampled (i.e. include full detail.)
56+
ELASTIC\_APM\_ENVIRONMENT | | Environment name, e.g. "production".
57+
ELASTIC\_APM\_FRAMEWORK\_NAME | | Framework name, e.g. "gin".
58+
ELASTIC\_APM\_FRAMEWORK\_VERSION | | Framework version, e.g. "1.0".
59+
ELASTIC\_APM\_SERVICE\_NAME | | Service name, e.g. "my-service". If this is unspecified, the agent will report the program binary name as the service name.
60+
ELASTIC\_APM\_SERVICE\_VERSION | | Service version, e.g. "1.0".
61+
ELASTIC\_APM\_HOSTNAME | | Override for the hostname.
62+
ELASTIC\_APM\_SANITIZE\_FIELD\_NAMES |[(1)](#(1))| A pattern to match names of cookies and form fields that should be redacted.
63+
64+
<a name="(1)">(1)</a> ELASTIC\_APM\_SANITIZE\_FIELD\_NAMES
65+
66+
By default, we redact the values of cookies and POST form fields that match the following regular expression:
67+
68+
`password|passwd|pwd|secret|.*key|.*token|.*session.*|.*credit.*|.*card.*`
69+
70+
The pattern specified in ELASTIC\_APM\_SANITIZE\_FIELD\_NAMES is treated
71+
case-insensitively by default. To override this behavior and match case-sensitively,
72+
wrap the value like `(?-i:<value>)`. For a full definition of Go's regular
73+
expression syntax, see https://golang.org/pkg/regexp/syntax/.
6274

6375
## Instrumentation
6476

env.go

+31
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package elasticapm
22

33
import (
4+
"fmt"
45
"math/rand"
56
"os"
7+
"regexp"
68
"strconv"
9+
"strings"
710
"time"
811

912
"github.com/pkg/errors"
@@ -14,12 +17,27 @@ const (
1417
envMaxQueueSize = "ELASTIC_APM_MAX_QUEUE_SIZE"
1518
envMaxSpans = "ELASTIC_APM_TRANSACTION_MAX_SPANS"
1619
envTransactionSampleRate = "ELASTIC_APM_TRANSACTION_SAMPLE_RATE"
20+
envSanitizeFieldNames = "ELASTIC_APM_SANITIZE_FIELD_NAMES"
1721

1822
defaultFlushInterval = 10 * time.Second
1923
defaultMaxTransactionQueueSize = 500
2024
defaultMaxSpans = 500
2125
)
2226

27+
var (
28+
defaultSanitizedFieldNames = regexp.MustCompile(fmt.Sprintf("(?i:%s)", strings.Join([]string{
29+
"password",
30+
"passwd",
31+
"pwd",
32+
"secret",
33+
".*key",
34+
".*token",
35+
".*session.*",
36+
".*credit.*",
37+
".*card.*",
38+
}, "|")))
39+
)
40+
2341
func initialFlushInterval() (time.Duration, error) {
2442
value := os.Getenv(envFlushInterval)
2543
if value == "" {
@@ -85,3 +103,16 @@ func initialSampler() (Sampler, error) {
85103
source := rand.NewSource(time.Now().Unix())
86104
return NewRatioSampler(ratio, source), nil
87105
}
106+
107+
func initialSanitizedFieldNamesRegexp() (*regexp.Regexp, error) {
108+
value := os.Getenv(envSanitizeFieldNames)
109+
if value == "" {
110+
return defaultSanitizedFieldNames, nil
111+
}
112+
re, err := regexp.Compile(fmt.Sprintf("(?i:%s)", value))
113+
if err != nil {
114+
_, err = regexp.Compile(value)
115+
return nil, errors.Wrapf(err, "invalid %s value", envSanitizeFieldNames)
116+
}
117+
return re, nil
118+
}

env_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package elasticapm_test
22

33
import (
44
"context"
5+
"net/http"
6+
"net/http/httptest"
57
"os"
68
"os/exec"
79
"testing"
@@ -11,6 +13,7 @@ import (
1113
"github.com/stretchr/testify/require"
1214

1315
"github.com/elastic/apm-agent-go"
16+
"github.com/elastic/apm-agent-go/contrib/apmhttp"
1417
"github.com/elastic/apm-agent-go/model"
1518
"github.com/elastic/apm-agent-go/transport/transporttest"
1619
)
@@ -91,6 +94,39 @@ func testTracerTransactionRateEnv(t *testing.T, envValue string, ratio float64)
9194
assert.InDelta(t, N*ratio, sampled, N*0.02) // allow 2% error
9295
}
9396

97+
func TestTracerSanitizeFieldNamesEnvInvalid(t *testing.T) {
98+
os.Setenv("ELASTIC_APM_SANITIZE_FIELD_NAMES", "oy(")
99+
defer os.Unsetenv("ELASTIC_APM_SANITIZE_FIELD_NAMES")
100+
101+
_, err := elasticapm.NewTracer("tracer_testing", "")
102+
assert.EqualError(t, err, "invalid ELASTIC_APM_SANITIZE_FIELD_NAMES value: error parsing regexp: missing closing ): `oy(`")
103+
}
104+
105+
func TestTracerSanitizeFieldNamesEnv(t *testing.T) {
106+
testTracerSanitizeFieldNamesEnv(t, "secRet", "[REDACTED]")
107+
testTracerSanitizeFieldNamesEnv(t, "nada", "top")
108+
}
109+
110+
func testTracerSanitizeFieldNamesEnv(t *testing.T, envValue, expect string) {
111+
os.Setenv("ELASTIC_APM_SANITIZE_FIELD_NAMES", envValue)
112+
defer os.Unsetenv("ELASTIC_APM_SANITIZE_FIELD_NAMES")
113+
114+
tracer, transport := transporttest.NewRecorderTracer()
115+
defer tracer.Close()
116+
117+
w := httptest.NewRecorder()
118+
req, _ := http.NewRequest("GET", "http://server.testing/", nil)
119+
req.AddCookie(&http.Cookie{Name: "secret", Value: "top"})
120+
h := &apmhttp.Handler{Handler: http.NotFoundHandler(), Tracer: tracer}
121+
h.ServeHTTP(w, req)
122+
tracer.Flush(nil)
123+
124+
tx := transport.Payloads()[0].Transactions()[0]
125+
assert.Equal(t, tx.Context.Request.Cookies, model.Cookies{
126+
{Name: "secret", Value: expect},
127+
})
128+
}
129+
94130
func TestTracerServiceNameEnvSanitizationSpecified(t *testing.T) {
95131
testTracerServiceNameSanitization(
96132
t, "TestTracerServiceNameEnvSanitizationSpecified",

sanitizer.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package elasticapm
2+
3+
import (
4+
"bytes"
5+
"regexp"
6+
7+
"github.com/elastic/apm-agent-go/model"
8+
)
9+
10+
const redacted = "[REDACTED]"
11+
12+
// sanitizeRequest sanitizes HTTP request data, redacting
13+
// the values of cookies and forms whose corresponding keys
14+
// match the given regular expression.
15+
func sanitizeRequest(r *model.Request, re *regexp.Regexp) {
16+
var anyCookiesRedacted bool
17+
for _, c := range r.Cookies {
18+
if !re.MatchString(c.Name) {
19+
continue
20+
}
21+
c.Value = redacted
22+
anyCookiesRedacted = true
23+
}
24+
if anyCookiesRedacted && r.Headers != nil {
25+
var b bytes.Buffer
26+
for i, c := range r.Cookies {
27+
if i != 0 {
28+
b.WriteRune(';')
29+
}
30+
b.WriteString(c.String())
31+
}
32+
r.Headers.Cookie = b.String()
33+
}
34+
if r.Body != nil && r.Body.Form != nil {
35+
for key, values := range r.Body.Form {
36+
if !re.MatchString(key) {
37+
continue
38+
}
39+
for i := range values {
40+
values[i] = redacted
41+
}
42+
}
43+
}
44+
}

sanitizer_test.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package elasticapm_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/elastic/apm-agent-go/contrib/apmhttp"
12+
"github.com/elastic/apm-agent-go/model"
13+
"github.com/elastic/apm-agent-go/transport/transporttest"
14+
)
15+
16+
func TestSanitizeRequest(t *testing.T) {
17+
tracer, transport := transporttest.NewRecorderTracer()
18+
defer tracer.Close()
19+
20+
mux := http.NewServeMux()
21+
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
22+
w.WriteHeader(http.StatusTeapot)
23+
}))
24+
h := &apmhttp.Handler{
25+
Handler: mux,
26+
Tracer: tracer,
27+
}
28+
29+
w := httptest.NewRecorder()
30+
req, _ := http.NewRequest("GET", "http://server.testing/", nil)
31+
for _, c := range []*http.Cookie{
32+
{Name: "secret", Value: "top"},
33+
{Name: "Custom-Credit-Card-Number", Value: "top"},
34+
{Name: "sessionid", Value: "123"},
35+
{Name: "user_id", Value: "456"},
36+
} {
37+
req.AddCookie(c)
38+
}
39+
h.ServeHTTP(w, req)
40+
tracer.Flush(nil)
41+
42+
payloads := transport.Payloads()
43+
require.Len(t, payloads, 1)
44+
transactions := payloads[0].Transactions()
45+
require.Len(t, transactions, 1)
46+
47+
tx := transactions[0]
48+
assert.Equal(t, tx.Context.Request.Cookies, model.Cookies{
49+
{Name: "Custom-Credit-Card-Number", Value: "[REDACTED]"},
50+
{Name: "secret", Value: "[REDACTED]"},
51+
{Name: "sessionid", Value: "[REDACTED]"},
52+
{Name: "user_id", Value: "456"},
53+
})
54+
assert.Equal(t,
55+
"secret=[REDACTED];Custom-Credit-Card-Number=[REDACTED];sessionid=[REDACTED];user_id=456",
56+
tx.Context.Request.Headers.Cookie,
57+
)
58+
}
59+
60+
func TestSetSanitizedFieldNamesNone(t *testing.T) {
61+
testSetSanitizedFieldNames(t, "top")
62+
}
63+
64+
func TestSetSanitizedFieldNamesCaseSensitivity(t *testing.T) {
65+
// patterns are matched case-insensitively by default
66+
testSetSanitizedFieldNames(t, "[REDACTED]", "Secret")
67+
68+
// patterns can be made case-sensitive by clearing the "i" flag.
69+
testSetSanitizedFieldNames(t, "top", "(?-i:Secret)")
70+
}
71+
72+
func testSetSanitizedFieldNames(t *testing.T, expect string, sanitized ...string) {
73+
tracer, transport := transporttest.NewRecorderTracer()
74+
defer tracer.Close()
75+
tracer.SetSanitizedFieldNames(sanitized...)
76+
77+
mux := http.NewServeMux()
78+
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
79+
w.WriteHeader(http.StatusTeapot)
80+
}))
81+
h := &apmhttp.Handler{
82+
Handler: mux,
83+
Tracer: tracer,
84+
}
85+
86+
w := httptest.NewRecorder()
87+
req, _ := http.NewRequest("GET", "http://server.testing/", nil)
88+
req.AddCookie(&http.Cookie{Name: "secret", Value: "top"})
89+
h.ServeHTTP(w, req)
90+
tracer.Flush(nil)
91+
92+
payloads := transport.Payloads()
93+
require.Len(t, payloads, 1)
94+
transactions := payloads[0].Transactions()
95+
require.Len(t, transactions, 1)
96+
97+
tx := transactions[0]
98+
assert.Equal(t, tx.Context.Request.Cookies, model.Cookies{
99+
{Name: "secret", Value: expect},
100+
})
101+
assert.Equal(t, "secret="+expect, tx.Context.Request.Headers.Cookie)
102+
}

0 commit comments

Comments
 (0)