v0.281.0: 'Done' asks the probe first; sign-up closed after the first admin (decision 47); R-713 code-bound values refused, ${NAME|base64}
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -60,23 +61,63 @@ func expandAfterInstall(cmd []string, allowed []string, env map[string]string) (
|
||||
for _, n := range allowed {
|
||||
ok[n] = true
|
||||
}
|
||||
var missing []string
|
||||
var missing, unsafe []string
|
||||
out := make([]string, len(cmd))
|
||||
for i, a := range cmd {
|
||||
// R-713 (v0.281.0): where does the value land? Its OWN argument ("${X}") or a plain argument
|
||||
// ("--password=${X}", "admin:${X}") cannot be read as code — any value. Text with spaces, quotes or
|
||||
// brackets around it ('… "${X}" …' in an Elixir or Python string) can: there the raw value must not hold a
|
||||
// quote, a backslash, $, {, }, a backtick or a line break. `${X|base64}` is always safe (letters, digits,
|
||||
// +, /, =): the template decodes it in its own code (claper).
|
||||
codeShaped := !argumentShaped(a)
|
||||
out[i] = os.Expand(a, func(k string) string {
|
||||
if !ok[k] || env[k] == "" {
|
||||
name, enc, _ := strings.Cut(k, "|")
|
||||
if !ok[name] || env[name] == "" {
|
||||
missing = append(missing, name)
|
||||
return ""
|
||||
}
|
||||
v := env[name]
|
||||
switch enc {
|
||||
case "":
|
||||
case "base64":
|
||||
return base64.StdEncoding.EncodeToString([]byte(v))
|
||||
default:
|
||||
missing = append(missing, k)
|
||||
return ""
|
||||
}
|
||||
return env[k]
|
||||
if codeShaped && strings.ContainsAny(v, codeUnsafeChars) {
|
||||
unsafe = append(unsafe, name)
|
||||
}
|
||||
return v
|
||||
})
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return nil, fmt.Errorf("after_install: %v not declared in env or has no value — not run", missing)
|
||||
}
|
||||
if len(unsafe) > 0 {
|
||||
return nil, fmt.Errorf("after_install: the value of %v would be read as code (it holds a quote, a backslash, $, {, }, a backtick or a line break) — not run; the template should pass it as its own argument or as ${NAME|base64}", unsafe)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// codeUnsafeChars can end a string or start an interpolation in the code a command carries.
|
||||
const codeUnsafeChars = "'\"\\$`{}\n\r\x00"
|
||||
|
||||
// argumentShaped: with every ${…} removed, the element is empty or plain argument text (a flag, a name, a "user:"
|
||||
// prefix) — nothing that can open or close a string in code.
|
||||
func argumentShaped(a string) bool {
|
||||
rest := os.Expand(a, func(string) string { return "" })
|
||||
for _, r := range rest {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
|
||||
case strings.ContainsRune("-_.:=/@+,", r):
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RunAfterInstall runs the app's after_install once, after a FRESH install (main.go's deploy-done hook).
|
||||
// Returns (ran, error). Records the outcome in app.yaml either way.
|
||||
func (m *Manager) RunAfterInstall(name string, wait time.Duration) (bool, error) {
|
||||
|
||||
Reference in New Issue
Block a user