-
Notifications
You must be signed in to change notification settings - Fork 259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scrubbing env vars from logs #1315
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package log | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"sync/atomic" | ||
|
||
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" | ||
) | ||
|
||
// This package scrubs objects of potentially sensitive information to pass to logging | ||
|
||
type genMap = map[string]interface{} | ||
type scrubberFunc func(genMap) error | ||
|
||
const ScrubbedReplacement = "<scrubbed>" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't need to be exported right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, ill also unexport it |
||
|
||
var ( | ||
ErrUnknownType = errors.New("encoded object is of unknown type") | ||
|
||
// case sensitive keywords, so "env" is not a substring on "Environment" | ||
_scrubKeywords = [][]byte{[]byte("env"), []byte("Environment")} | ||
|
||
_scrub int32 | ||
) | ||
|
||
// SetScrubbing enables scrubbing | ||
func SetScrubbing(enable bool) { | ||
v := int32(0) // cant convert from bool to int32 directly | ||
if enable { | ||
v = 1 | ||
} | ||
atomic.StoreInt32(&_scrub, v) | ||
} | ||
|
||
// IsScrubbingEnabled checks if scrubbing is enabled | ||
func IsScrubbingEnabled() bool { | ||
v := atomic.LoadInt32(&_scrub) | ||
return v != 0 | ||
} | ||
|
||
// ScrubProcessParameters scrubs HCS Create Process requests with config parameters of | ||
// type internal/hcs/schema2.ScrubProcessParameters (aka hcsshema.ScrubProcessParameters) | ||
func ScrubProcessParameters(s string) (string, error) { | ||
// todo: deal with v1 ProcessConfig | ||
b := []byte(s) | ||
if !IsScrubbingEnabled() || !hasKeywords(b) || !json.Valid(b) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be returning an error if the string isn't a valid json? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it could be a go-formatted string, or some other parameter that is passed in erroneously, in which case I felt it best to just return it as is |
||
return s, nil | ||
} | ||
|
||
pp := hcsschema.ProcessParameters{} | ||
if err := json.Unmarshal(b, &pp); err != nil { | ||
return "", err | ||
} | ||
pp.Environment = map[string]string{ScrubbedReplacement: ScrubbedReplacement} | ||
|
||
buf := bytes.NewBuffer(b[:0]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did we need to use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since we control the byte slice here (from converting the string to a byte string), we can reuse the storage |
||
if err := encode(buf, pp); err != nil { | ||
return "", err | ||
} | ||
return buf.String(), nil | ||
} | ||
|
||
// ScrubBridgeCreate scrubs requests sent over the bridge of type | ||
// internal/gcs/protocol.containerCreate wrapping an internal/hcsoci.linuxHostedSystem | ||
func ScrubBridgeCreate(b []byte) ([]byte, error) { | ||
return scrubBytes(b, scrubLinuxHostedSystem) | ||
} | ||
|
||
func scrubLinuxHostedSystem(m genMap) error { | ||
if !isRequestBase(m) { | ||
return ErrUnknownType | ||
} | ||
if m, ok := index(m, "ContainerConfig"); ok { | ||
if m, ok := index(m, "OciSpecification"); ok { | ||
if m, ok := index(m, "process"); ok { | ||
if _, ok := m["env"]; ok { | ||
m["env"] = []string{ScrubbedReplacement} | ||
return nil | ||
} | ||
} | ||
} | ||
} | ||
return ErrUnknownType | ||
} | ||
|
||
// ScrubBridgeExecProcess scrubs requests sent over the bridge of type | ||
// internal/gcs/protocol.containerExecuteProcess | ||
func ScrubBridgeExecProcess(b []byte) ([]byte, error) { | ||
return scrubBytes(b, scrubExecuteProcess) | ||
} | ||
|
||
func scrubExecuteProcess(m genMap) error { | ||
if !isRequestBase(m) { | ||
return ErrUnknownType | ||
} | ||
if m, ok := index(m, "Settings"); ok { | ||
if ss, ok := m["ProcessParameters"]; ok { | ||
// ProcessParameters is a json encoded struct passed as a regular sting field | ||
s, ok := ss.(string) | ||
if !ok { | ||
return ErrUnknownType | ||
} | ||
|
||
s, err := ScrubProcessParameters(s) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
m["ProcessParameters"] = s | ||
return nil | ||
} | ||
} | ||
return ErrUnknownType | ||
} | ||
|
||
func scrubBytes(b []byte, scrub scrubberFunc) ([]byte, error) { | ||
if !IsScrubbingEnabled() || !hasKeywords(b) || !json.Valid(b) { | ||
return b, nil | ||
} | ||
|
||
m := make(genMap) | ||
if err := json.Unmarshal(b, &m); err != nil { | ||
return nil, err | ||
} | ||
|
||
// could use regexp, but if the env strings contain braces, the regexp fails | ||
// parsing into individual structs would require access to private structs | ||
if err := scrub(m); err != nil { | ||
return nil, err | ||
} | ||
|
||
buf := &bytes.Buffer{} | ||
if err := encode(buf, m); err != nil { | ||
return nil, err | ||
} | ||
return buf.Bytes(), nil | ||
} | ||
|
||
func encode(buf *bytes.Buffer, v interface{}) error { | ||
enc := json.NewEncoder(buf) | ||
enc.SetEscapeHTML(false) | ||
if err := enc.Encode(v); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func isRequestBase(m genMap) bool { | ||
// neither of these are (currently) `omitempty` | ||
_, a := m["ActivityId"] | ||
_, c := m["ContainerId"] | ||
return a && c | ||
} | ||
Comment on lines
+150
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a huge fan of parsing all of the things in this file in string format, this seems like an unreliable way to determine fields. The alternative was marshalling and unmarshalling right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, but that required exporting the internal bridge protocol structs, and I didnt know if that was an issue |
||
|
||
// combination `m, ok := m[s]` and `m, ok := m.(genMap)` | ||
func index(m genMap, s string) (genMap, bool) { | ||
if m, ok := m[s]; ok { | ||
mm, ok := m.(genMap) | ||
return mm, ok | ||
} | ||
|
||
return m, false | ||
} | ||
|
||
func hasKeywords(b []byte) bool { | ||
for _, bb := range _scrubKeywords { | ||
if bytes.Contains(b, bb) { | ||
return true | ||
} | ||
} | ||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did we add this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default behavior is to escape characters for HTML, and since the replacement is
<scrubbed>
, that gets encoded as\u003cscrubbed\u0033
, which i felt would cause issues for pattern matching down the line