Skip to content

Commit 7d83b81

Browse files
authored
fix(env): restrict envar expansion to inside only string literal (#146)
1 parent 72b57ae commit 7d83b81

2 files changed

Lines changed: 224 additions & 55 deletions

File tree

env/env.go

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"os"
77
"path/filepath"
88
"regexp"
9-
"strings"
109

1110
"github.com/aws/aws-sdk-go-v2/service/ecs"
1211
)
@@ -44,6 +43,10 @@ const TaskHealthCheckTimeout = "CAGE_TASK_HEALTH_CHECK_TIMEOUT"
4443
const TaskStoppedTimeout = "CAGE_TASK_STOPPED_TIMEOUT"
4544
const ServiceStableTimeout = "CAGE_SERVICE_STABLE_TIMEOUT"
4645

46+
var (
47+
envarLiteralRegexp = regexp.MustCompile(`\$\{([^}\r\n]+)\}`)
48+
)
49+
4750
func EnsureEnvars(
4851
dest *Envars,
4952
) error {
@@ -70,7 +73,7 @@ func LoadServiceDefinition(dir string) (*ecs.CreateServiceInput, error) {
7073
if noSvc != nil {
7174
return nil, fmt.Errorf("no 'service.json' found in %s", dir)
7275
}
73-
if err := ReadAndUnmarshalJson(svcPath, &service); err != nil {
76+
if err := readAndUnmarshalJson(svcPath, &service); err != nil {
7477
return nil, fmt.Errorf("failed to read and unmarshal 'service.json': %s", err)
7578
}
7679
return &service, nil
@@ -83,7 +86,7 @@ func LoadTaskDefinition(dir string) (*ecs.RegisterTaskDefinitionInput, error) {
8386
if noTd != nil {
8487
return nil, fmt.Errorf("no 'task-definition.json' found in %s", dir)
8588
}
86-
if err := ReadAndUnmarshalJson(tdPath, &td); err != nil {
89+
if err := readAndUnmarshalJson(tdPath, &td); err != nil {
8790
return nil, fmt.Errorf("failed to read and unmarshal 'task-definition.json': %s", err)
8891
}
8992
return &td, nil
@@ -113,29 +116,82 @@ func MergeEnvars(dest *Envars, src *Envars) {
113116
}
114117
}
115118

116-
func ReadAndUnmarshalJson(path string, dest interface{}) error {
117-
if d, err := ReadFileAndApplyEnvars(path); err != nil {
119+
func readAndUnmarshalJson(path string, dest any) error {
120+
if b, err := os.ReadFile(path); err != nil {
121+
return err
122+
} else if d, err := applyEnvarsToJSON(b, path); err != nil {
118123
return err
119124
} else if err := json.Unmarshal(d, dest); err != nil {
120125
return err
121126
}
122127
return nil
123128
}
124129

125-
func ReadFileAndApplyEnvars(path string) ([]byte, error) {
126-
d, err := os.ReadFile(path)
130+
func applyEnvarsToJSON(d []byte, path string) ([]byte, error) {
131+
var js any
132+
if err := json.Unmarshal(d, &js); err != nil {
133+
return nil, err
134+
}
135+
applied, err := applyEnvarsToJSONValue(js, path)
127136
if err != nil {
128137
return nil, err
129138
}
130-
str := string(d)
131-
reg := regexp.MustCompile(`\${(.+?)}`)
132-
submatches := reg.FindAllStringSubmatch(str, -1)
133-
for _, m := range submatches {
134-
if envar, ok := os.LookupEnv(m[1]); ok {
135-
str = strings.Replace(str, m[0], envar, -1)
136-
} else {
137-
return nil, fmt.Errorf("envar literal '%s' found in %s but was not defined", m[0], path)
139+
return json.Marshal(applied)
140+
}
141+
142+
func applyEnvarsToJSONValue(value any, path string) (any, error) {
143+
switch v := value.(type) {
144+
case map[string]any:
145+
for key, child := range v {
146+
if envarLiteralRegexp.MatchString(key) {
147+
return nil, fmt.Errorf("envar literal found in JSON object key '%s' in %s; envars can only be used in JSON string values", key, path)
148+
}
149+
applied, err := applyEnvarsToJSONValue(child, path)
150+
if err != nil {
151+
return nil, err
152+
}
153+
v[key] = applied
154+
}
155+
return v, nil
156+
case []any:
157+
for i, child := range v {
158+
applied, err := applyEnvarsToJSONValue(child, path)
159+
if err != nil {
160+
return nil, err
161+
}
162+
v[i] = applied
163+
}
164+
return v, nil
165+
case string:
166+
return applyEnvarsToString(v, path)
167+
default:
168+
return v, nil
169+
}
170+
}
171+
172+
func applyEnvarsToString(str string, path string) (string, error) {
173+
var replaceErr error
174+
replaced := envarLiteralRegexp.ReplaceAllStringFunc(str, func(literal string) string {
175+
if replaceErr != nil {
176+
return literal
138177
}
178+
envar, err := lookupEnvar(literal, path)
179+
if err != nil {
180+
replaceErr = err
181+
return literal
182+
}
183+
return envar
184+
})
185+
return replaced, replaceErr
186+
}
187+
188+
func lookupEnvar(literal string, path string) (string, error) {
189+
match := envarLiteralRegexp.FindStringSubmatch(literal)
190+
if len(match) != 2 {
191+
return "", fmt.Errorf("invalid envar literal '%s' found in %s", literal, path)
192+
}
193+
if envar, ok := os.LookupEnv(match[1]); ok {
194+
return envar, nil
139195
}
140-
return []byte(str), nil
196+
return "", fmt.Errorf("envar literal '%s' found in %s but was not defined", literal, path)
141197
}

0 commit comments

Comments
 (0)