Hashicorp Vault may expose sensitive log information in github.com/hashicorp/vault
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
1.15.0 → fixed in 1.15.5
Details
Hashicorp Vault may expose sensitive log information in github.com/hashicorp/vault
The fix
VAULT-23122: Audit fix for 'log_raw' issue (#24968) (#24974)
audit/entry_formatter.go+20 −4
@@ -80,7 +80,7 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (*evreturn nil, fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter)}-a, ok := e.Payload.(*auditEvent)+a, ok := e.Payload.(*AuditEvent)if !ok {return nil, fmt.Errorf("%s: cannot parse event payload: %w", op, event.ErrInvalidParameter)}@@ -155,10 +155,26 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (*evresult = append([]byte(f.prefix), result...)}-// Store the final format.-e.FormattedAs(f.config.RequiredFormat.String(), result)+// Copy some properties from the event (and audit event) and store the+// format for the next (sink) node to Process.+a2 := &AuditEvent{+ID: a.ID,+Version: a.Version,+Subtype: a.Subtype,+Timestamp: a.Timestamp,+Data: data, // Use the cloned data here rather than a pointer to the original.+}++e2 := &eventlogger.Event{+Type: e.Type,+CreatedAt: e.CreatedAt,+Formatted: make(map[string][]byte), // we are about to set this ourselves.+Payload: a2,+}++e2.FormattedAs(f.config.RequiredFormat.String(), result)-return e, nil+return e2, nil}// FormatRequest attempts to format the specified logical.LogInput into a RequestEntry.
audit/entry_formatter_test.go+302 −43
@@ -5,49 +5,23 @@ package auditimport ("context"+"encoding/json"+"errors"+"fmt"+"strings""testing""time"-"github.com/hashicorp/vault/internal/observability/event"-+"github.com/hashicorp/eventlogger""github.com/hashicorp/vault/helper/namespace"-+"github.com/hashicorp/vault/internal/observability/event"+"github.com/hashicorp/vault/sdk/helper/jsonutil"+"github.com/hashicorp/vault/sdk/helper/salt""github.com/hashicorp/vault/sdk/logical"--"github.com/hashicorp/eventlogger"+"github.com/mitchellh/copystructure""github.com/stretchr/testify/require")-// fakeEvent will return a new fake event containing audit data based on the-// specified subtype, format and logical.LogInput.-func fakeEvent(tb testing.TB, subtype subtype, format format, input *logical.LogInput) *eventlogger.Event {-tb.Helper()--date := time.Date(2023, time.July, 11, 15, 49, 10, 0o0, time.Local)--auditEvent, err := NewEvent(subtype,-WithID("123"),-WithNow(date),-)-require.NoError(tb, err)-require.NotNil(tb, auditEvent)-require.Equal(tb, "123", auditEvent.ID)-require.Equal(tb, "v0.1", auditEvent.Version)-require.Equal(tb, subtype, auditEvent.Subtype)-require.Equal(tb, date, auditEvent.Timestamp)--auditEvent.Data = input--e := &eventlogger.Event{-Type: eventlogger.EventType(event.AuditType),-CreatedAt: auditEvent.Timestamp,-Formatted: make(map[string][]byte),-Payload: auditEvent,-}--return e-}-// TestNewEntryFormatter ensures we can create new EntryFormatter structs.func TestNewEntryFormatter(t *testing.T) {tests := map[string]struct {@@ -297,7 +271,7 @@ func TestEntryFormatter_Process(t *testing.T) {tc := tct.Run(name, func(t *testing.T) {t.Parallel()-e := fakeEvent(t, tc.Subtype, tc.RequiredFormat, tc.Data)+e := fakeEvent(t, tc.Subtype, tc.Data)require.NotNil(t, e)ss := newStaticSalt(t)@@ -317,18 +291,16 @@ func TestEntryFormatter_Process(t *testing.T) {}processed, err := f.Process(ctx, e)-b, found := e.Format(string(tc.RequiredFormat))switch {case tc.IsErrorExpected:require.Error(t, err)require.EqualError(t, err, tc.ExpectedErrorMessage)require.Nil(t, processed)-require.False(t, found)-require.Nil(t, b)default:require.NoError(t, err)require.NotNil(t, processed)+b, found := processed.Format(string(tc.RequiredFormat))require.True(t, found)require.NotNil(t, b)}@@ -381,20 +353,413 @@ func BenchmarkAuditFileSink_Process(b *testing.B) {require.NotNil(b, sink)// Generate the event-event := fakeEvent(b, RequestType, JSONFormat, in)-require.NotNil(b, event)+e := fakeEvent(b, RequestType, in)+require.NotNil(b, e)b.ResetTimer()b.RunParallel(func(pb *testing.PB) {for pb.Next() {-event, err = formatter.Process(ctx, event)+e2, err := formatter.Process(ctx, e)if err != nil {panic(err)}-_, err := sink.Process(ctx, event)+_, err = sink.Process(ctx, e2)if err != nil {panic(err)}}})}++// TestEntryFormatter_Process_JSON ensures that the JSON output we get matches what+// we expect for the specified LogInput.+func TestEntryFormatter_Process_JSON(t *testing.T) {+ss := newStaticSalt(t)++expectedResultStr := fmt.Sprintf(testFormatJSONReqBasicStrFmt, ss.salt.GetIdentifiedHMAC("foo"))++issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")+cases := map[string]struct {+Auth *logical.Auth+Req *logical.Request+Err error+Prefix string+ExpectedStr string+}{+"auth, request": {+&logical.Auth{+ClientToken: "foo",+Accessor: "bar",+DisplayName: "testtoken",+EntityID: "foobarentity",+NoDefaultPolicy: true,+Policies: []string{"root"},+TokenType: logical.TokenTypeService,+LeaseOptions: logical.LeaseOptions{+TTL: time.Hour * 4,+IssueTime: issueTime,+},+},+&logical.Request{+Operation: logical.UpdateOperation,+Path: "/foo",+Connection: &logical.Connection{+RemoteAddr: "127.0.0.1",+},+WrapInfo: &logical.RequestWrapInfo{+TTL: 60 * time.Second,+},+Headers: map[string][]string{+"foo": {"bar"},+},+},+errors.New("this is an error"),+"",+expectedResultStr,+},+"auth, request with prefix": {+&logical.Auth{+ClientToken: "foo",+Accessor: "bar",+EntityID: "foobarentity",+DisplayName: "testtoken",+NoDefaultPolicy: true,+Policies: []string{"root"},+TokenType: logical.TokenTypeService,+LeaseOptions: logical.LeaseOptions{+TTL: time.Hour * 4,+IssueTime: issueTime,+},+},+&logical.Request{+Operation: logical.UpdateOperation,+Path: "/foo",+Connection: &logical.Connection{+RemoteAddr: "127.0.0.1",+},+WrapInfo: &logical.RequestWrapInfo{+TTL: 60 * time.Second,+},+Headers: map[string][]string{+"foo": {"bar"},+},+},+errors.New("this is an error"),+"@cee: ",+expectedResultStr,+},+}++for name, tc := range cases {+cfg, err := NewFormatterConfig(WithHMACAccessor(false))+require.NoError(t, err)+formatter, err := NewEntryFormatter(cfg, ss, WithPrefix(tc.Prefix))+require.NoError(t, err)++in := &logical.LogInput{+Auth: tc.Auth,+Request: tc.Req,+OuterErr: tc.Err,+}++// Create an audit event and more generic eventlogger.event to allow us+// to process (format).+auditEvent, err := NewEvent(RequestType)+require.NoError(t, err)+auditEvent.Data = in++e := &eventlogger.Event{+Type: eventlogger.EventType(event.AuditType.String()),+CreatedAt: time.Now(),+Formatted: make(map[string][]byte),+Payload: auditEvent,+}++e2, err := formatter.Process(namespace.RootContext(nil), e)+require.NoErrorf(t, err, "bad: %s\nerr: %s", name, err)++jsonBytes, ok := e2.Format(JSONFormat.String())+require.True(t, ok)+require.Positive(t, len(jsonBytes))++if !strings.HasPrefix(string(jsonBytes), tc.Prefix) {+t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, expectedResultStr, tc.Prefix)+}++expectedJSON := new(RequestEntry)++if err := jsonutil.DecodeJSON([]byte(expectedResultStr), &expectedJSON); err != nil {+t.Fatalf("bad json: %s", err)+}+expectedJSON.Request.Namespace = &Namespace{ID: "root"}++actualJSON := new(RequestEntry)+if err := jsonutil.DecodeJSON(jsonBytes[len(tc.Prefix):], &actualJSON); err != nil {+t.Fatalf("bad json: %s", err)+}++expectedJSON.Time = actualJSON.Time++expectedBytes, err := json.Marshal(expectedJSON)+if err != nil {+t.Fatalf("unable to marshal json: %s", err)+}++if !strings.HasSuffix(strings.TrimSpace(string(jsonBytes)), string(expectedBytes)) {+t.Fatalf("bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q", name, string(jsonBytes), string(expectedBytes))+}+}+}++// TestEntryFormatter_Process_JSONx ensures that the JSONx output we get matches what+// we expect for the specified LogInput.+func TestEntryFormatter_Process_JSONx(t *testing.T) {+s, err := salt.NewSalt(context.Background(), nil, nil)+require.NoError(t, err)+tempStaticSalt := &staticSalt{salt: s}++fooSalted := s.GetIdentifiedHMAC("foo")+issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")++cases := map[string]struct {+Auth *logical.Auth+Req *logical.Request+Err error+Prefix string+Result string+ExpectedStr string+}{+"auth, request": {+&logical.Auth{+ClientToken: "foo",+Accessor: "bar",+DisplayName: "testtoken",+EntityID: "foobarentity",+NoDefaultPolicy: true,+Policies: []string{"root"},+TokenType: logical.TokenTypeService,+LeaseOptions: logical.LeaseOptions{+TTL: time.Hour * 4,+IssueTime: issueTime,+},+},+&logical.Request{+ID: "request",+ClientToken: "foo",+ClientTokenAccessor: "bar",+Operation: logical.UpdateOperation,+Path: "/foo",+Connection: &logical.Connection{+RemoteAddr: "127.0.0.1",+},+WrapInfo: &logical.RequestWrapInfo{+TTL: 60 * time.Second,+},+Headers: map[string][]string{+"foo": {"bar"},+},+PolicyOverride: true,+},+errors.New("this is an error"),+"",+"",+fmt.Sprintf(`<json:object name="auth"><json:string name="accessor">bar</json:string><json:string name="client_token">%s</json:string><json:string name="display_name">testtoken</json:string><json:string name="entity_id">foobarentity</json:string><json:boolean name="no_default_policy">true</json:boolean><json:array name="policies"><json:string>root</json:string></json:array><json:string name="token_issue_time">2020-05-28T13:40:18-05:00</json:string><json:number name="token_ttl">14400</json:number><json:string name="token_type">service</json:string></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token">%s</json:string><json:string name="client_token_accessor">bar</json:string><json:object name="headers"><json:array name="foo"><json:string>bar</json:string></json:array></json:object><json:string name="id">request</json:string><json:object name="namespace"><json:string name="id">root</json:string></json:object><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:boolean name="policy_override">true</json:boolean><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,+fooSalted, fooSalted),+},+"auth, request with prefix": {+&logical.Auth{+ClientToken: "foo",+Accessor: "bar",+DisplayName: "testtoken",+NoDefaultPolicy: true,+EntityID: "foobarentity",+Policies: []string{"root"},+TokenType: logical.TokenTypeService,+LeaseOptions: logical.LeaseOptions{+TTL: time.Hour * 4,+IssueTime: issueTime,+},+},+&logical.Request{+ID: "request",+ClientToken: "foo",+ClientTokenAccessor: "bar",+Operation: logical.UpdateOperation,+Path: "/foo",+Connection: &logical.Connection{+RemoteAddr: "127.0.0.1",+},+WrapInfo: &logical.RequestWrapInfo{+TTL: 60 * time.Second,+},+Headers: map[string][]string{+"foo": {"bar"},+},+PolicyOverride: true,+},+errors.New("this is an error"),+"",+"@cee: ",+fmt.Sprintf(`<json:object name="auth"><json:string name="accessor">bar</json:string><json:string name="client_token">%s</json:string><json:string name="display_name">testtoken</json:string><json:string name="entity_id">foobarentity</json:string><json:boolean name="no_default_policy">true</json:boolean><json:array name="policies"><json:string>root</json:string></json:array><json:string name="token_issue_time">2020-05-28T13:40:18-05:00</json:string><json:number name="token_ttl">14400</json:number><json:string name="token_type">service</json:string></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token">%s</json:string><json:string name="client_token_accessor">bar</json:string><json:object name="headers"><json:array name="foo"><json:string>bar</json:string></json:array></json:object><json:string name="id">request</json:string><json:object name="namespace"><json:string name="id">root</json:string></json:object><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:boolean name="policy_override">true</json:boolean><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,+fooSalted, fooSalted),+},+}++for name, tc := range cases {+cfg, err := NewFormatterConfig(+WithOmitTime(true),+WithHMACAccessor(false),+WithFormat(JSONxFormat.String()),+)+require.NoError(t, err)+formatter, err := NewEntryFormatter(cfg, tempStaticSalt, WithPrefix(tc.Prefix))+require.NoError(t, err)+require.NotNil(t, formatter)++in := &logical.LogInput{+Auth: tc.Auth,+Request: tc.Req,+OuterErr: tc.Err,+}++// Create an audit event and more generic eventlogger.event to allow us+// to process (format).+auditEvent, err := NewEvent(RequestType)+require.NoError(t, err)+auditEvent.Data = in++e := &eventlogger.Event{+Type: eventlogger.EventType(event.AuditType.String()),+CreatedAt: time.Now(),+Formatted: make(map[string][]byte),+Payload: auditEvent,+}++e2, err := formatter.Process(namespace.RootContext(nil), e)+require.NoErrorf(t, err, "bad: %s\nerr: %s", name, err)++jsonxBytes, ok := e2.Format(JSONxFormat.String())+require.True(t, ok)+require.Positive(t, len(jsonxBytes))++if !strings.HasPrefix(string(jsonxBytes), tc.Prefix) {+t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, tc.Result, tc.Prefix)+}++if !strings.HasSuffix(strings.TrimSpace(string(jsonxBytes)), string(tc.ExpectedStr)) {+t.Fatalf(+"bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q",+name, strings.TrimSpace(string(jsonxBytes)), string(tc.ExpectedStr))+}+}+}++// TestEntryFormatter_Process_NoMutation tests that the event returned by an+// EntryFormatter.Process method is not the same as the one that it accepted.+func TestEntryFormatter_Process_NoMutation(t *testing.T) {… diff truncated
audit/event.go+5 −5
@@ -12,8 +12,8 @@ import (// NewEvent should be used to create an audit event. The subtype field is needed// for audit events. It will generate an ID if no ID is supplied. Supported// options: WithID, WithNow.-func NewEvent(s subtype, opt ...Option) (*auditEvent, error) {-const op = "audit.newEvent"+func NewEvent(s subtype, opt ...Option) (*AuditEvent, error) {+const op = "audit.NewEvent"// Get the default optionsopts, err := getOpts(opt...)@@ -30,7 +30,7 @@ func NewEvent(s subtype, opt ...Option) (*auditEvent, error) {}}-audit := &auditEvent{+audit := &AuditEvent{ID: opts.withID,Timestamp: opts.withNow,Version: version,@@ -44,8 +44,8 @@ func NewEvent(s subtype, opt ...Option) (*auditEvent, error) {}// validate attempts to ensure the audit event in its present state is valid.-func (a *auditEvent) validate() error {-const op = "audit.(auditEvent).validate"+func (a *AuditEvent) validate() error {+const op = "audit.(AuditEvent).validate"if a == nil {return fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter)
audit/event_test.go+16 −16
@@ -29,21 +29,21 @@ func TestAuditEvent_new(t *testing.T) {Subtype: subtype(""),Format: format(""),IsErrorExpected: true,-ExpectedErrorMessage: "audit.newEvent: audit.(auditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter",+ExpectedErrorMessage: "audit.NewEvent: audit.(AuditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter",},"empty-Option": {Options: []Option{},Subtype: subtype(""),Format: format(""),IsErrorExpected: true,-ExpectedErrorMessage: "audit.newEvent: audit.(auditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter",+ExpectedErrorMessage: "audit.NewEvent: audit.(AuditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter",},"bad-id": {Options: []Option{WithID("")},Subtype: ResponseType,Format: JSONFormat,IsErrorExpected: true,-ExpectedErrorMessage: "audit.newEvent: error applying options: id cannot be empty",+ExpectedErrorMessage: "audit.NewEvent: error applying options: id cannot be empty",},"good": {Options: []Option{@@ -108,22 +108,22 @@ func TestAuditEvent_new(t *testing.T) {// TestAuditEvent_Validate exercises the validation for an audit event.func TestAuditEvent_Validate(t *testing.T) {tests := map[string]struct {-Value *auditEvent+Value *AuditEventIsErrorExpected boolExpectedErrorMessage string}{"nil": {Value: nil,IsErrorExpected: true,-ExpectedErrorMessage: "audit.(auditEvent).validate: event is nil: invalid parameter",+ExpectedErrorMessage: "audit.(AuditEvent).validate: event is nil: invalid parameter",},"default": {-Value: &auditEvent{},+Value: &AuditEvent{},IsErrorExpected: true,-ExpectedErrorMessage: "audit.(auditEvent).validate: missing ID: invalid parameter",+ExpectedErrorMessage: "audit.(AuditEvent).validate: missing ID: invalid parameter",},"id-empty": {-Value: &auditEvent{+Value: &AuditEvent{ID: "",Version: version,Subtype: RequestType,@@ -131,10 +131,10 @@ func TestAuditEvent_Validate(t *testing.T) {Data: nil,},IsErrorExpected: true,-ExpectedErrorMessage: "audit.(auditEvent).validate: missing ID: invalid parameter",+ExpectedErrorMessage: "audit.(AuditEvent).validate: missing ID: invalid parameter",},"version-fiddled": {-Value: &auditEvent{+Value: &AuditEvent{ID: "audit_123",Version: "magic-v2",Subtype: RequestType,@@ -142,10 +142,10 @@ func TestAuditEvent_Validate(t *testing.T) {Data: nil,},IsErrorExpected: true,-ExpectedErrorMessage: "audit.(auditEvent).validate: event version unsupported: invalid parameter",+ExpectedErrorMessage: "audit.(AuditEvent).validate: event version unsupported: invalid parameter",},"subtype-fiddled": {-Value: &auditEvent{+Value: &AuditEvent{ID: "audit_123",Version: version,Subtype: subtype("moon"),@@ -153,10 +153,10 @@ func TestAuditEvent_Validate(t *testing.T) {Data: nil,},IsErrorExpected: true,-ExpectedErrorMessage: "audit.(auditEvent).validate: audit.(subtype).validate: 'moon' is not a valid event subtype: invalid parameter",+ExpectedErrorMessage: "audit.(AuditEvent).validate: audit.(subtype).validate: 'moon' is not a valid event subtype: invalid parameter",},"default-time": {-Value: &auditEvent{+Value: &AuditEvent{ID: "audit_123",Version: version,Subtype: ResponseType,@@ -164,10 +164,10 @@ func TestAuditEvent_Validate(t *testing.T) {Data: nil,},IsErrorExpected: true,-ExpectedErrorMessage: "audit.(auditEvent).validate: event timestamp cannot be the zero time instant: invalid parameter",+ExpectedErrorMessage: "audit.(AuditEvent).validate: event timestamp cannot be the zero time instant: invalid parameter",},"valid": {-Value: &auditEvent{+Value: &AuditEvent{ID: "audit_123",Version: version,Subtype: ResponseType,
audit/sink_wrapper.go+2 −2
@@ -12,7 +12,7 @@ import ()// SinkWrapper is a wrapper for any kind of Sink Node that processes events-// containing an auditEvent payload.+// containing an AuditEvent payload.type SinkWrapper struct {Name stringSink eventlogger.Node@@ -23,7 +23,7 @@ type SinkWrapper struct {// once this method returns.func (s *SinkWrapper) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {defer func() {-auditEvent, ok := e.Payload.(*auditEvent)+auditEvent, ok := e.Payload.(*AuditEvent)if ok {metrics.MeasureSince([]string{"audit", s.Name, auditEvent.Subtype.MetricTag()}, e.CreatedAt)}
audit/types.go+2 −2
@@ -35,8 +35,8 @@ type subtype string// format defines types of format audit events support.type format string-// auditEvent is the audit event.-type auditEvent struct {+// AuditEvent is the audit event.+type AuditEvent struct {ID string `json:"id"`Version string `json:"version"`Subtype subtype `json:"subtype"` // the subtype of the audit event.
changelog/24968.txt+3 −0
@@ -0,0 +1,3 @@+```release-note:bug+audit: Fix bug where use of 'log_raw' option could result in other devices logging raw audit data+```
internal/observability/event/sink_socket.go+1 −2
@@ -10,9 +10,8 @@ import ("sync""time"-"github.com/hashicorp/go-multierror"-"github.com/hashicorp/eventlogger"+"github.com/hashicorp/go-multierror")var _ eventlogger.Node = (*SocketSink)(nil)
More files changed — see the full commit.
References
- ADVISORYhttps://github.com/advisories/GHSA-vgh3-mwxq-rcp8
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2024-0831
- FIXhttps://github.com/hashicorp/vault/commit/2a72f2a8a5b57de88c22a2a94c4a5f08c6f3770b
- WEBhttps://developer.hashicorp.com/vault/docs/upgrading/upgrade-to-1.15.x#audit-devices-could-log-raw-data-despite-configuration
- WEBhttps://discuss.hashicorp.com/t/hcsec-2024-01-vault-may-expose-sensitive-information-when-configuring-an-audit-log-device/62311
- WEBhttps://security.netapp.com/advisory/ntap-20240223-0005