From 5d7bc9515ed25fbd6aa8529e849367935e4e786f Mon Sep 17 00:00:00 2001 From: Mikhail Swift Date: Wed, 18 May 2022 13:57:45 -0400 Subject: [PATCH] feat: block known sensitive environment variables Uses the list from https://github.com/Puliczek/awesome-list-of-secrets-in-environment-variables/blob/main/raw_list.txt but allows for the configuration of custom block lists. Signed-off-by: Mikhail Swift --- pkg/attestation/commandrun/commandrun.go | 19 ++- pkg/attestation/commandrun/tracing_linux.go | 38 +++--- pkg/attestation/environment/blocklist.go | 112 ++++++++++++++++++ pkg/attestation/environment/environment.go | 46 +++++-- .../environment/environment_test.go | 41 +++++++ 5 files changed, 224 insertions(+), 32 deletions(-) create mode 100644 pkg/attestation/environment/blocklist.go create mode 100644 pkg/attestation/environment/environment_test.go diff --git a/pkg/attestation/commandrun/commandrun.go b/pkg/attestation/commandrun/commandrun.go index ba5d9428..d6369aaa 100644 --- a/pkg/attestation/commandrun/commandrun.go +++ b/pkg/attestation/commandrun/commandrun.go @@ -21,6 +21,7 @@ import ( "os/exec" "github.com/testifysec/witness/pkg/attestation" + "github.com/testifysec/witness/pkg/attestation/environment" "github.com/testifysec/witness/pkg/cryptoutil" ) @@ -62,8 +63,17 @@ func WithSilent(silent bool) Option { } } +func WithEnvironmentBlockList(blockList map[string]struct{}) Option { + return func(cr *CommandRun) { + cr.environmentBlockList = blockList + } +} + func New(opts ...Option) *CommandRun { - cr := &CommandRun{} + cr := &CommandRun{ + environmentBlockList: environment.DefaultBlockList(), + } + for _, opt := range opts { opt(cr) } @@ -91,9 +101,10 @@ type CommandRun struct { ExitCode int `json:"exitcode"` Processes []ProcessInfo `json:"processes,omitempty"` - silent bool - materials map[string]cryptoutil.DigestSet - enableTracing bool + silent bool + materials map[string]cryptoutil.DigestSet + enableTracing bool + environmentBlockList map[string]struct{} } func (rc *CommandRun) Attest(ctx *attestation.AttestationContext) error { diff --git a/pkg/attestation/commandrun/tracing_linux.go b/pkg/attestation/commandrun/tracing_linux.go index 61cb7447..b852c3b2 100644 --- a/pkg/attestation/commandrun/tracing_linux.go +++ b/pkg/attestation/commandrun/tracing_linux.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/testifysec/witness/pkg/attestation" + "github.com/testifysec/witness/pkg/attestation/environment" "github.com/testifysec/witness/pkg/cryptoutil" "github.com/testifysec/witness/pkg/log" "golang.org/x/sys/unix" @@ -37,11 +38,12 @@ const ( ) type ptraceContext struct { - parentPid int - mainProgram string - processes map[int]*ProcessInfo - exitCode int - hash []crypto.Hash + parentPid int + mainProgram string + processes map[int]*ProcessInfo + exitCode int + hash []crypto.Hash + environmentBlockList map[string]struct{} } func enableTracing(c *exec.Cmd) { @@ -52,10 +54,11 @@ func enableTracing(c *exec.Cmd) { func (r *CommandRun) trace(c *exec.Cmd, actx *attestation.AttestationContext) ([]ProcessInfo, error) { pctx := &ptraceContext{ - parentPid: c.Process.Pid, - mainProgram: c.Path, - processes: make(map[int]*ProcessInfo), - hash: actx.Hashes(), + parentPid: c.Process.Pid, + mainProgram: c.Path, + processes: make(map[int]*ProcessInfo), + hash: actx.Hashes(), + environmentBlockList: r.environmentBlockList, } if err := pctx.runTrace(); err != nil { @@ -175,7 +178,13 @@ func (p *ptraceContext) handleSyscall(pid int, regs unix.PtraceRegs) error { environ, err := os.ReadFile(envinLocation) if err == nil { - procInfo.Environ = cleanString(string(environ)) + allVars := strings.Split(string(environ), "\x00") + filteredEnviron := make([]string, 0) + environment.FilterEnvironmentArray(allVars, p.environmentBlockList, func(_, _, varStr string) { + filteredEnviron = append(filteredEnviron, varStr) + }) + + procInfo.Environ = strings.Join(filteredEnviron, " ") } cmdline, err := os.ReadFile(cmdlineLocation) @@ -201,8 +210,8 @@ func (p *ptraceContext) handleSyscall(pid int, regs unix.PtraceRegs) error { if err != nil { return err } - procInfo := p.getProcInfo(pid) + procInfo := p.getProcInfo(pid) digestSet, err := cryptoutil.CalculateDigestSetFromFile(file, p.hash) if err != nil { return err @@ -271,9 +280,7 @@ func cleanString(s string) string { func getPPIDFromStatus(status []byte) (int, error) { statusStr := string(status) - lines := strings.Split(statusStr, "\n") - for _, line := range lines { if strings.Contains(line, "PPid:") { parts := strings.Split(line, ":") @@ -281,14 +288,13 @@ func getPPIDFromStatus(status []byte) (int, error) { return strconv.Atoi(ppid) } } + return 0, nil } func getSpecBypassIsVulnFromStatus(status []byte) bool { statusStr := string(status) - lines := strings.Split(statusStr, "\n") - for _, line := range lines { if strings.Contains(line, "Speculation_Store_Bypass:") { parts := strings.Split(line, ":") @@ -296,8 +302,8 @@ func getSpecBypassIsVulnFromStatus(status []byte) bool { if strings.Contains(isVuln, "vulnerable") { return true } - } } + return false } diff --git a/pkg/attestation/environment/blocklist.go b/pkg/attestation/environment/blocklist.go new file mode 100644 index 00000000..9677ba8e --- /dev/null +++ b/pkg/attestation/environment/blocklist.go @@ -0,0 +1,112 @@ +// Copyright 2021 The Witness Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package environment + +// sourced from https://github.com/Puliczek/awesome-list-of-secrets-in-environment-variables/blob/main/raw_list.txt +func DefaultBlockList() map[string]struct{} { + return map[string]struct{}{ + "AWS_ACCESS_KEY_ID": {}, + "AWS_SECRET_ACCESS_KEY": {}, + "AMAZON_AWS_ACCESS_KEY_ID": {}, + "AMAZON_AWS_SECRET_ACCESS_KEY": {}, + "ALGOLIA_API_KEY": {}, + "AZURE_CLIENT_ID": {}, + "AZURE_CLIENT_SECRET": {}, + "AZURE_USERNAME": {}, + "AZURE_PASSWORD": {}, + "MSI_ENDPOINT": {}, + "MSI_SECRET": {}, + "binance_api": {}, + "binance_secret": {}, + "BITTREX_API_KEY": {}, + "BITTREX_API_SECRET": {}, + "CF_PASSWORD": {}, + "CF_USERNAME": {}, + "CODECLIMATE_REPO_TOKEN": {}, + "COVERALLS_REPO_TOKEN": {}, + "CIRCLE_TOKEN": {}, + "DIGITALOCEAN_ACCESS_TOKEN": {}, + "DOCKER_EMAIL": {}, + "DOCKER_PASSWORD": {}, + "DOCKER_USERNAME": {}, + "DOCKERHUB_PASSWORD": {}, + "FACEBOOK_APP_ID": {}, + "FACEBOOK_APP_SECRET": {}, + "FACEBOOK_ACCESS_TOKEN": {}, + "FIREBASE_TOKEN": {}, + "FOSSA_API_KEY": {}, + "GH_TOKEN": {}, + "GH_ENTERPRISE_TOKEN": {}, + "GOOGLE_APPLICATION_CREDENTIALS": {}, + "GOOGLE_API_KEY": {}, + "CI_DEPLOY_USER": {}, + "CI_DEPLOY_PASSWORD": {}, + "GITLAB_USER_LOGIN": {}, + "CI_JOB_JWT": {}, + "CI_JOB_JWT_V2": {}, + "CI_JOB_TOKEN": {}, + "HEROKU_API_KEY": {}, + "HEROKU_API_USER": {}, + "MAILGUN_API_KEY": {}, + "MCLI_PRIVATE_API_KEY": {}, + "MCLI_PUBLIC_API_KEY": {}, + "NGROK_TOKEN": {}, + "NGROK_AUTH_TOKEN": {}, + "NPM_AUTH_TOKEN": {}, + "OKTA_CLIENT_ORGURL": {}, + "OKTA_CLIENT_TOKEN": {}, + "OKTA_OAUTH2_CLIENTSECRET": {}, + "OKTA_OAUTH2_CLIENTID": {}, + "OKTA_AUTHN_GROUPID": {}, + "OS_USERNAME": {}, + "OS_PASSWORD": {}, + "PERCY_TOKEN": {}, + "SAUCE_ACCESS_KEY": {}, + "SAUCE_USERNAME": {}, + "SENTRY_AUTH_TOKEN": {}, + "SLACK_TOKEN": {}, + "SNYK_TOKEN": {}, + "square_access_token": {}, + "square_oauth_secret": {}, + "STRIPE_API_KEY": {}, + "STRIPE_DEVICE_NAME": {}, + "SURGE_TOKEN": {}, + "SURGE_LOGIN": {}, + "TWILIO_ACCOUNT_SID": {}, + "CONSUMER_KEY": {}, + "CONSUMER_SECRET": {}, + "TRAVIS_SUDO": {}, + "TRAVIS_OS_NAME": {}, + "TRAVIS_SECURE_ENV_VARS": {}, + "VAULT_TOKEN": {}, + "VAULT_CLIENT_KEY": {}, + "TOKEN": {}, + "VULTR_ACCESS": {}, + "VULTR_SECRET": {}, + } +} + +// FilterEnvironmentArray expects an array of strings representing environment variables. Each element of the array is expected to be in the format of "KEY=VALUE". +// blockList is the list of elements to filter from variables, and for each element of variables that does not appear in the blockList onAllowed will be called. +func FilterEnvironmentArray(variables []string, blockList map[string]struct{}, onAllowed func(key, val, orig string)) { + for _, v := range variables { + key, val := splitVariable(v) + if _, inBlockList := blockList[key]; inBlockList { + continue + } + + onAllowed(key, val, v) + } +} diff --git a/pkg/attestation/environment/environment.go b/pkg/attestation/environment/environment.go index 01dd988d..8d3c7bfd 100644 --- a/pkg/attestation/environment/environment.go +++ b/pkg/attestation/environment/environment.go @@ -40,10 +40,28 @@ type Attestor struct { Hostname string `json:"hostname"` Username string `json:"username"` Variables map[string]string `json:"variables,omitempty"` + + blockList map[string]struct{} +} + +type Option func(*Attestor) + +func WithBlockList(blockList map[string]struct{}) Option { + return func(a *Attestor) { + a.blockList = blockList + } } -func New() *Attestor { - return &Attestor{} +func New(opts ...Option) *Attestor { + attestor := &Attestor{ + blockList: DefaultBlockList(), + } + + for _, opt := range opts { + opt(attestor) + } + + return attestor } func (a *Attestor) Name() string { @@ -70,17 +88,21 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { a.Username = user.Username } - variables := os.Environ() - for _, v := range variables { - parts := strings.SplitN(v, "=", 2) - key := parts[0] - val := "" - if len(parts) > 1 { - val = parts[1] - } - + FilterEnvironmentArray(os.Environ(), a.blockList, func(key, val, _ string) { a.Variables[key] = val - } + }) return nil } + +// splitVariable splits a string representing an environment variable in the format of +// "KEY=VAL" and returns the key and val separately. +func splitVariable(v string) (key, val string) { + parts := strings.SplitN(v, "=", 2) + key = parts[0] + if len(parts) > 1 { + val = parts[1] + } + + return +} diff --git a/pkg/attestation/environment/environment_test.go b/pkg/attestation/environment/environment_test.go new file mode 100644 index 00000000..e4206686 --- /dev/null +++ b/pkg/attestation/environment/environment_test.go @@ -0,0 +1,41 @@ +// Copyright 2021 The Witness Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package environment + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/testifysec/witness/pkg/attestation" +) + +func TestEnvironment(t *testing.T) { + attestor := New() + ctx, err := attestation.NewContext([]attestation.Attestor{attestor}) + require.NoError(t, err) + + t.Setenv("AWS_ACCESS_KEY_ID", "super secret") + origVars := os.Environ() + require.NoError(t, attestor.Attest(ctx)) + for _, env := range origVars { + origKey, _ := splitVariable(env) + if _, inBlockList := attestor.blockList[origKey]; inBlockList { + require.NotContains(t, attestor.Variables, origKey) + } else { + require.Contains(t, attestor.Variables, origKey) + } + } +}