Security context
Critical· 9.8GHSA-57gg-cj55-q5g2 CVE-2020-25816CWE-613Published May 24, 2022

Token leases could outlive their TTL in HashiCorp Vault

Research this vulnerability

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.0.0-beta1 → fixed in 1.5.4

Details

HashiCorp Vault and Vault Enterprise 1.0 before 1.5.4 have Incorrect Access Control.

The fix

Port changes from enterprise lease fix

Brian Kassouf· Sep 22, 2020, 09:33 PM+469f192878110
helper/forwarding/types.pb.go+1 1
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
-// protoc v3.12.2
+// protoc v3.13.0
// source: helper/forwarding/types.proto
package forwarding
helper/identity/mfa/types.pb.go+1 1
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
-// protoc v3.12.2
+// protoc v3.13.0
// source: helper/identity/mfa/types.proto
package mfa
helper/identity/types.pb.go+1 1
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
-// protoc v3.12.2
+// protoc v3.13.0
// source: helper/identity/types.proto
package identity
helper/storagepacker/types.pb.go+1 1
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
-// protoc v3.12.2
+// protoc v3.13.0
// source: helper/storagepacker/types.proto
package storagepacker
physical/raft/chunking_test.go+10 1
@@ -81,7 +81,7 @@ func TestRaft_Chunking_Lifecycle(t *testing.T) {
t.Log("tearing down cluster")
require.NoError(b.TeardownCluster(nil))
- require.NoError(b.fsm.db.Close())
+ require.NoError(b.fsm.getDB().Close())
require.NoError(b.stableStore.(*raftboltdb.BoltStore).Close())
t.Log("starting new backend")
@@ -195,6 +195,15 @@ func TestRaft_Chunking_AppliedIndex(t *testing.T) {
t.Fatal(err)
}
+ // Write a value to fastforward the index
+ err = raft.Put(context.Background(), &physical.Entry{
+ Key: "key",
+ Value: []byte("test"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
currentIndex := raft.AppliedIndex()
// Write some data
for i := 0; i < 10; i++ {
physical/raft/fsm.go+25 0
@@ -77,6 +77,9 @@ type FSM struct {
logger log.Logger
noopRestore bool
+ // applyDelay is used to simulate a slow apply in tests
+ applyDelay time.Duration
+
db *bolt.DB
// retoreCb is called after we've restored a snapshot
@@ -118,6 +121,21 @@ func NewFSM(path string, logger log.Logger) (*FSM, error) {
return f, nil
}
+func (f *FSM) getDB() *bolt.DB {
+ f.l.RLock()
+ defer f.l.RUnlock()
+
+ return f.db
+}
+
+// SetFSMDelay adds a delay to the FSM apply. This is used in tests to simulate
+// a slow apply.
+func (r *RaftBackend) SetFSMDelay(delay time.Duration) {
+ r.fsm.l.Lock()
+ r.fsm.applyDelay = delay
+ r.fsm.l.Unlock()
+}
+
func (f *FSM) openDBFile(dbPath string) error {
if len(dbPath) == 0 {
return errors.New("can not open empty filename")
@@ -222,6 +240,9 @@ func writeSnapshotMetaToDB(metadata *raft.SnapshotMeta, db *bolt.DB) error {
}
func (f *FSM) witnessSnapshot(metadata *raft.SnapshotMeta) error {
+ f.l.RLock()
+ defer f.l.RUnlock()
+
err := writeSnapshotMetaToDB(metadata, f.db)
if err != nil {
return err
@@ -448,6 +469,10 @@ func (f *FSM) ApplyBatch(logs []*raft.Log) []interface{} {
f.l.RLock()
defer f.l.RUnlock()
+ if f.applyDelay > 0 {
+ time.Sleep(f.applyDelay)
+ }
+
err = f.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(dataBucketName)
for _, commandRaw := range commands {
physical/raft/raft.go+5 2
@@ -768,11 +768,14 @@ func (b *RaftBackend) AppliedIndex() uint64 {
b.l.RLock()
defer b.l.RUnlock()
- if b.raft == nil {
+ if b.fsm == nil {
return 0
}
- return b.raft.AppliedIndex()
+ // We use the latest index that the FSM has seen here, which may be behind
+ // raft.AppliedIndex() due to the async nature of the raft library.
+ indexState, _ := b.fsm.LatestState()
+ return indexState.Index
}
// RemovePeer removes the given peer ID from the raft cluster. If the node is
physical/raft/raft_test.go+2 2
@@ -80,7 +80,7 @@ func getRaftWithDir(t testing.TB, bootstrap bool, noStoreState bool, raftDir str
}
for {
- if backend.AppliedIndex() >= 2 {
+ if backend.raft.AppliedIndex() >= 2 {
break
}
}
@@ -156,7 +156,7 @@ func compareFSMsWithErr(t *testing.T, fsm1, fsm2 *FSM) error {
return fmt.Errorf("configs did not match: %+v != %+v", config1, config2)
}
- return compareDBs(t, fsm1.db, fsm2.db, false)
+ return compareDBs(t, fsm1.getDB(), fsm2.getDB(), false)
}
func compareDBs(t *testing.T, boltDB1, boltDB2 *bolt.DB, dataOnly bool) error {
More files changed — see the full commit.

References