From 372a3279cc7fcb4c665178974ff111f1adf268da Mon Sep 17 00:00:00 2001 From: AdamMagued Date: Fri, 9 Oct 2026 14:53:07 +0300 Subject: [PATCH] fix(key): prevent stack overflow on cyclic key variable references Signed-off-by: AdamMagued --- key.go | 31 +++++++++++++++++++++--- key_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/key.go b/key.go index 800d04a..88089d5 100644 --- a/key.go +++ b/key.go @@ -140,14 +140,26 @@ func (k *Key) NestedValues() []string { // transformValue takes a raw value and transforms to its final string. func (k *Key) transformValue(val string) string { + res, _ := k.transformValueWithVisited(val, nil) + return res +} + +func (k *Key) transformValueWithVisited(val string, visited map[*Key]struct{}) (string, bool) { if k.s.f.ValueMapper != nil { val = k.s.f.ValueMapper(val) } // Fail-fast if no indicate char found for recursive value if !strings.Contains(val, "%") { - return val + return val, false + } + + if visited == nil { + visited = make(map[*Key]struct{}) } + visited[k] = struct{}{} + defer delete(visited, k) + for i := 0; i < depthValues; i++ { vr := varPattern.FindString(val) if len(vr) == 0 { @@ -167,12 +179,25 @@ func (k *Key) transformValue(val string) string { // and returns the value as-is. break } + if nk == k { + // Stop when key references itself to prevent cycle. + return val, true + } + } + + if _, ok := visited[nk]; ok { + return val, true + } + + newVal, hasCycle := nk.transformValueWithVisited(nk.value, visited) + if hasCycle { + return val, true } // Substitute by new value and take off leading '%(' and trailing ')s'. - val = strings.ReplaceAll(val, vr, nk.String()) + val = strings.ReplaceAll(val, vr, newVal) } - return val + return val, false } // String returns string representation of value. diff --git a/key_test.go b/key_test.go index e57a7bd..19a1a9c 100644 --- a/key_test.go +++ b/key_test.go @@ -654,4 +654,74 @@ ENDPOINT = https://%(ACCOUNT_ID)s.r2.cloudflarestorage.com assert.Equal(t, "https://abc123.r2.cloudflarestorage.com", f.Section("").Key("ENDPOINT").String()) }) + + t.Run("self-reference in default section", func(t *testing.T) { + f, err := Load([]byte("a = %(a)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "%(a)s", f.Section("").Key("a").String()) + }) + + t.Run("self-reference in section", func(t *testing.T) { + f, err := Load([]byte("[s]\na = %(a)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "%(a)s", f.Section("s").Key("a").String()) + }) + + t.Run("mutual reference between keys in section", func(t *testing.T) { + f, err := Load([]byte("[s]\na = %(b)s\nb = %(a)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "%(b)s", f.Section("s").Key("a").String()) + assert.Equal(t, "%(a)s", f.Section("s").Key("b").String()) + }) + + t.Run("mutual reference between keys in default section", func(t *testing.T) { + f, err := Load([]byte("a = %(b)s\nb = %(a)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "%(b)s", f.Section("").Key("a").String()) + assert.Equal(t, "%(a)s", f.Section("").Key("b").String()) + }) + + t.Run("multi-key cyclic reference", func(t *testing.T) { + f, err := Load([]byte("[s]\na = %(b)s\nb = %(c)s\nc = %(a)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "%(b)s", f.Section("s").Key("a").String()) + assert.Equal(t, "%(c)s", f.Section("s").Key("b").String()) + assert.Equal(t, "%(a)s", f.Section("s").Key("c").String()) + }) + + t.Run("partial cycle with valid reference", func(t *testing.T) { + f, err := Load([]byte("[s]\nvalid = hello\na = %(valid)s %(b)s\nb = %(a)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "hello %(b)s", f.Section("s").Key("a").String()) + }) + + t.Run("diamond reference without cycle", func(t *testing.T) { + f, err := Load([]byte("[s]\nd = leaf\nb = %(d)s\nc = %(d)s\na = %(b)s and %(c)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + assert.Equal(t, "leaf and leaf", f.Section("s").Key("a").String()) + }) + + t.Run("shadows with cyclic references", func(t *testing.T) { + f, err := LoadSources(LoadOptions{AllowShadows: true}, []byte("[s]\na = %(b)s\na = %(a)s\nb = %(a)s\n")) + require.NoError(t, err) + require.NotNil(t, f) + + vals := f.Section("s").Key("a").ValueWithShadows() + require.Len(t, vals, 2) + assert.Equal(t, "%(b)s", f.Section("s").Key("a").String()) + }) }