Skip to content

Commit 24572ad

Browse files
ankitpatel96mx-psi
authored andcommitted
Restrict characters in environment variable names (open-telemetry#10183)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Restricts Environment Variable names according to the [restrictions](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/file-configuration.md#environment-variable-substitution) published by the OpenTelemetry Configuration Working Group. Changes both the expand converter and the env provider. Defines one regex to be used for both of these. <!-- Issue number if applicable --> #### Link to tracking issue Fixes open-telemetry#9531 <!--Describe what testing was performed and which tests were added.--> #### Testing Unit tests <!--Please delete paragraphs that you did not use before submitting.--> --------- Co-authored-by: Pablo Baeyens <[email protected]>
1 parent 7c5c11f commit 24572ad

File tree

9 files changed

+183
-19
lines changed

9 files changed

+183
-19
lines changed

.chloggen/env-var-name-verify.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: breaking
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: envprovider
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Restricts Environment Variable names. Environment variable names must now be ASCII only and start with a letter or an underscore, and can only contain underscores, letters, or numbers.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [9531]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: ['user']

confmap/converter/expandconverter/expand.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"go.uber.org/zap"
1313

1414
"go.opentelemetry.io/collector/confmap"
15+
"go.opentelemetry.io/collector/confmap/internal/envvar"
1516
)
1617

1718
type converter struct {
@@ -35,36 +36,50 @@ func newConverter(set confmap.ConverterSettings) confmap.Converter {
3536
}
3637

3738
func (c converter) Convert(_ context.Context, conf *confmap.Conf) error {
39+
var err error
3840
out := make(map[string]any)
3941
for _, k := range conf.AllKeys() {
40-
out[k] = c.expandStringValues(conf.Get(k))
42+
out[k], err = c.expandStringValues(conf.Get(k))
43+
if err != nil {
44+
return err
45+
}
4146
}
4247
return conf.Merge(confmap.NewFromStringMap(out))
4348
}
4449

45-
func (c converter) expandStringValues(value any) any {
50+
func (c converter) expandStringValues(value any) (any, error) {
51+
var err error
4652
switch v := value.(type) {
4753
case string:
4854
return c.expandEnv(v)
4955
case []any:
5056
nslice := make([]any, 0, len(v))
5157
for _, vint := range v {
52-
nslice = append(nslice, c.expandStringValues(vint))
58+
var nv any
59+
nv, err = c.expandStringValues(vint)
60+
if err != nil {
61+
return nil, err
62+
}
63+
nslice = append(nslice, nv)
5364
}
54-
return nslice
65+
return nslice, nil
5566
case map[string]any:
5667
nmap := map[string]any{}
5768
for mk, mv := range v {
58-
nmap[mk] = c.expandStringValues(mv)
69+
nmap[mk], err = c.expandStringValues(mv)
70+
if err != nil {
71+
return nil, err
72+
}
5973
}
60-
return nmap
74+
return nmap, nil
6175
default:
62-
return v
76+
return v, nil
6377
}
6478
}
6579

66-
func (c converter) expandEnv(s string) string {
67-
return os.Expand(s, func(str string) string {
80+
func (c converter) expandEnv(s string) (string, error) {
81+
var err error
82+
res := os.Expand(s, func(str string) string {
6883
// Matches on $VAR style environment variables
6984
// in order to make sure we don't log a warning for ${VAR}
7085
var regex = regexp.MustCompile(fmt.Sprintf(`\$%s`, regexp.QuoteMeta(str)))
@@ -80,6 +95,13 @@ func (c converter) expandEnv(s string) string {
8095
if str == "$" {
8196
return "$"
8297
}
98+
// For $ENV style environment variables os.Expand returns once it hits a character that isn't an underscore or
99+
// an alphanumeric character - so we cannot detect those malformed environment variables.
100+
// For ${ENV} style variables we can detect those kinds of env var names!
101+
if !envvar.ValidationRegexp.MatchString(str) {
102+
err = fmt.Errorf("environment variable %q has invalid name: must match regex %s", str, envvar.ValidationPattern)
103+
return ""
104+
}
83105
val, exists := os.LookupEnv(str)
84106
if !exists {
85107
c.logger.Warn("Configuration references unset environment variable", zap.String("name", str))
@@ -88,4 +110,5 @@ func (c converter) expandEnv(s string) string {
88110
}
89111
return val
90112
})
113+
return res, err
91114
}

confmap/converter/expandconverter/expand_test.go

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"go.opentelemetry.io/collector/confmap"
1919
"go.opentelemetry.io/collector/confmap/confmaptest"
20+
"go.opentelemetry.io/collector/confmap/internal/envvar"
2021
)
2122

2223
func TestNewExpandConverter(t *testing.T) {
@@ -176,13 +177,16 @@ func TestDeprecatedWarning(t *testing.T) {
176177
t.Setenv("PORT", "4317")
177178

178179
t.Setenv("HOST_NAME", "127.0.0.2")
179-
t.Setenv("HOST.NAME", "127.0.0.3")
180+
t.Setenv("HOSTNAME", "127.0.0.3")
181+
182+
t.Setenv("BAD!HOST", "127.0.0.2")
180183

181184
var testCases = []struct {
182185
name string
183186
input map[string]any
184187
expectedOutput map[string]any
185188
expectedWarnings []string
189+
expectedError error
186190
}{
187191
{
188192
name: "no warning",
@@ -193,6 +197,7 @@ func TestDeprecatedWarning(t *testing.T) {
193197
"test": "127.0.0.1:4317",
194198
},
195199
expectedWarnings: []string{},
200+
expectedError: nil,
196201
},
197202
{
198203
name: "one deprecated var",
@@ -203,6 +208,7 @@ func TestDeprecatedWarning(t *testing.T) {
203208
"test": "127.0.0.1:4317",
204209
},
205210
expectedWarnings: []string{"PORT"},
211+
expectedError: nil,
206212
},
207213
{
208214
name: "two deprecated vars",
@@ -213,6 +219,7 @@ func TestDeprecatedWarning(t *testing.T) {
213219
"test": "127.0.0.1:4317",
214220
},
215221
expectedWarnings: []string{"HOST", "PORT"},
222+
expectedError: nil,
216223
},
217224
{
218225
name: "one depracated serveral times",
@@ -225,25 +232,63 @@ func TestDeprecatedWarning(t *testing.T) {
225232
"test2": "127.0.0.1",
226233
},
227234
expectedWarnings: []string{"HOST"},
235+
expectedError: nil,
228236
},
229237
{
230238
name: "one warning",
231239
input: map[string]any{
232-
"test": "$HOST_NAME,${HOST.NAME}",
240+
"test": "$HOST_NAME,${HOSTNAME}",
233241
},
234242
expectedOutput: map[string]any{
235243
"test": "127.0.0.2,127.0.0.3",
236244
},
237245
expectedWarnings: []string{"HOST_NAME"},
246+
expectedError: nil,
247+
},
248+
{
249+
name: "malformed environment variable",
250+
input: map[string]any{
251+
"test": "${BAD!HOST}",
252+
},
253+
expectedOutput: map[string]any{
254+
"test": "blah",
255+
},
256+
expectedWarnings: []string{},
257+
expectedError: fmt.Errorf("environment variable \"BAD!HOST\" has invalid name: must match regex %s", envvar.ValidationRegexp),
258+
},
259+
{
260+
name: "malformed environment variable number",
261+
input: map[string]any{
262+
"test": "${2BADHOST}",
263+
},
264+
expectedOutput: map[string]any{
265+
"test": "blah",
266+
},
267+
expectedWarnings: []string{},
268+
expectedError: fmt.Errorf("environment variable \"2BADHOST\" has invalid name: must match regex %s", envvar.ValidationRegexp),
269+
},
270+
{
271+
name: "malformed environment variable unicode",
272+
input: map[string]any{
273+
"test": "${😊BADHOST}",
274+
},
275+
expectedOutput: map[string]any{
276+
"test": "blah",
277+
},
278+
expectedWarnings: []string{},
279+
expectedError: fmt.Errorf("environment variable \"😊BADHOST\" has invalid name: must match regex %s", envvar.ValidationRegexp),
238280
},
239281
}
282+
240283
for _, tt := range testCases {
241284
t.Run(tt.name, func(t *testing.T) {
242285
conf := confmap.NewFromStringMap(tt.input)
243286
conv, logs := NewTestConverter()
244-
require.NoError(t, conv.Convert(context.Background(), conf))
245-
246-
assert.Equal(t, tt.expectedOutput, conf.ToStringMap())
287+
err := conv.Convert(context.Background(), conf)
288+
assert.Equal(t, tt.expectedError, err)
289+
if tt.expectedError == nil {
290+
assert.Equal(t, tt.expectedOutput, conf.ToStringMap())
291+
}
247292
assert.Equal(t, len(tt.expectedWarnings), len(logs.All()))
248293
for i, variable := range tt.expectedWarnings {
249294
errorMsg := fmt.Sprintf(msgTemplate, variable)
@@ -253,6 +298,38 @@ func TestDeprecatedWarning(t *testing.T) {
253298
}
254299
}
255300

301+
func TestNewExpandConverterWithErrors(t *testing.T) {
302+
var testCases = []struct {
303+
name string // test case name (also file name containing config yaml)
304+
expectedError error
305+
}{
306+
{
307+
name: "expand-list-error.yaml",
308+
expectedError: fmt.Errorf("environment variable \"EXTRA_LIST_^VALUE_2\" has invalid name: must match regex %s", envvar.ValidationRegexp),
309+
},
310+
{
311+
name: "expand-list-map-error.yaml",
312+
expectedError: fmt.Errorf("environment variable \"EXTRA_LIST_MAP_V#ALUE_2\" has invalid name: must match regex %s", envvar.ValidationRegexp),
313+
},
314+
{
315+
name: "expand-map-error.yaml",
316+
expectedError: fmt.Errorf("environment variable \"EX#TRA\" has invalid name: must match regex %s", envvar.ValidationRegexp),
317+
},
318+
}
319+
320+
for _, test := range testCases {
321+
t.Run(test.name, func(t *testing.T) {
322+
conf, err := confmaptest.LoadConf(filepath.Join("testdata", "errors", test.name))
323+
require.NoError(t, err, "Unable to get config")
324+
325+
// Test that expanded configs are the same with the simple config with no env vars.
326+
err = createConverter().Convert(context.Background(), conf)
327+
328+
assert.Equal(t, test.expectedError, err)
329+
})
330+
}
331+
}
332+
256333
func createConverter() confmap.Converter {
257334
return NewFactory().Create(confmap.ConverterSettings{Logger: zap.NewNop()})
258335
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
test_map:
2+
extra_list:
3+
- "some list value_1"
4+
- "${EXTRA_LIST_^VALUE_2}"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
test_map:
2+
extra_list_map:
3+
- { recv.1: "some list map value_1",recv.2: "${EXTRA_LIST_MAP_V#ALUE_2}" }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
test_map:
2+
extra: "${EX#TRA}"

confmap/internal/envvar/pattern.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package envvar // import "go.opentelemetry.io/collector/confmap/internal/envvar"
5+
6+
import "regexp"
7+
8+
const ValidationPattern = `^[a-zA-Z_][a-zA-Z0-9_]*$`
9+
10+
var ValidationRegexp = regexp.MustCompile(ValidationPattern)

confmap/provider/envprovider/provider.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import (
1212
"go.uber.org/zap"
1313

1414
"go.opentelemetry.io/collector/confmap"
15+
"go.opentelemetry.io/collector/confmap/internal/envvar"
1516
"go.opentelemetry.io/collector/confmap/provider/internal"
1617
)
1718

18-
const schemeName = "env"
19+
const (
20+
schemeName = "env"
21+
)
1922

2023
type provider struct {
2124
logger *zap.Logger
@@ -40,6 +43,10 @@ func (emp *provider) Retrieve(_ context.Context, uri string, _ confmap.WatcherFu
4043
return nil, fmt.Errorf("%q uri is not supported by %q provider", uri, schemeName)
4144
}
4245
envVarName := uri[len(schemeName)+1:]
46+
if !envvar.ValidationRegexp.MatchString(envVarName) {
47+
return nil, fmt.Errorf("environment variable %q has invalid name: must match regex %s", envVarName, envvar.ValidationPattern)
48+
49+
}
4350
val, exists := os.LookupEnv(envVarName)
4451
if !exists {
4552
emp.logger.Warn("Configuration references unset environment variable", zap.String("name", envVarName))

confmap/provider/envprovider/provider_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package envprovider
55

66
import (
77
"context"
8+
"fmt"
89
"testing"
910

1011
"github.com/stretchr/testify/assert"
@@ -14,6 +15,7 @@ import (
1415

1516
"go.opentelemetry.io/collector/confmap"
1617
"go.opentelemetry.io/collector/confmap/confmaptest"
18+
"go.opentelemetry.io/collector/confmap/internal/envvar"
1719
)
1820

1921
const envSchemePrefix = schemeName + ":"
@@ -54,7 +56,7 @@ func TestInvalidYAML(t *testing.T) {
5456
}
5557

5658
func TestEnv(t *testing.T) {
57-
const envName = "default-config"
59+
const envName = "default_config"
5860
t.Setenv(envName, validYAML)
5961

6062
env := createProvider()
@@ -72,7 +74,7 @@ func TestEnv(t *testing.T) {
7274
}
7375

7476
func TestEnvWithLogger(t *testing.T) {
75-
const envName = "default-config"
77+
const envName = "default_config"
7678
t.Setenv(envName, validYAML)
7779
core, ol := observer.New(zap.WarnLevel)
7880
logger := zap.New(core)
@@ -93,7 +95,7 @@ func TestEnvWithLogger(t *testing.T) {
9395
}
9496

9597
func TestUnsetEnvWithLoggerWarn(t *testing.T) {
96-
const envName = "default-config"
98+
const envName = "default_config"
9799
core, ol := observer.New(zap.WarnLevel)
98100
logger := zap.New(core)
99101

@@ -114,8 +116,19 @@ func TestUnsetEnvWithLoggerWarn(t *testing.T) {
114116
assert.Equal(t, envName, logLine.Context[0].String)
115117
}
116118

119+
func TestEnvVarNameRestriction(t *testing.T) {
120+
const envName = "default%config"
121+
t.Setenv(envName, validYAML)
122+
123+
env := createProvider()
124+
ret, err := env.Retrieve(context.Background(), envSchemePrefix+envName, nil)
125+
assert.Equal(t, err, fmt.Errorf("environment variable \"default%%config\" has invalid name: must match regex %s", envvar.ValidationRegexp))
126+
assert.NoError(t, env.Shutdown(context.Background()))
127+
assert.Nil(t, ret)
128+
}
129+
117130
func TestEmptyEnvWithLoggerWarn(t *testing.T) {
118-
const envName = "default-config"
131+
const envName = "default_config"
119132
t.Setenv(envName, "")
120133

121134
core, ol := observer.New(zap.InfoLevel)

0 commit comments

Comments
 (0)