Merge commit from fork
security: GHSA-prj9-97mp-mwh2 (HIGH) Treat all ot_ system arguments as reserved, preventing RCE
This commit is contained in:
commit
6ca25bbdb4
|
|
@ -10,6 +10,7 @@ A full list of argument types are below;
|
||||||
| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
|
| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults.
|
||||||
| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
|
| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation
|
||||||
| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
|
| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`.
|
||||||
|
| shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers.
|
||||||
| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
|
| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`.
|
||||||
| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
|
| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc.
|
||||||
| email | xref:args/input.adoc[Textbox] | An email address.
|
| email | xref:args/input.adoc[Textbox] | An email address.
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ function getInputType(arg) {
|
||||||
return 'checkbox'
|
return 'checkbox'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.type === 'ascii_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
|
if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') {
|
||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ReservedArgumentNamePrefix is reserved for OliveTin-injected system arguments.
|
||||||
|
const ReservedArgumentNamePrefix = "ot_"
|
||||||
|
|
||||||
// Action represents the core functionality of OliveTin - commands that show up
|
// Action represents the core functionality of OliveTin - commands that show up
|
||||||
// as buttons in the UI.
|
// as buttons in the UI.
|
||||||
type Action struct {
|
type Action struct {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,34 @@ func (cfg *Config) Sanitize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.sanitizeDashboardsForInlineActions()
|
cfg.sanitizeDashboardsForInlineActions()
|
||||||
|
|
||||||
|
if err := cfg.validateReservedActionArgumentNames(); err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) validateReservedActionArgumentNames() error {
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
if err := action.validateReservedArgumentNames(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (action *Action) validateReservedArgumentNames() error {
|
||||||
|
if action == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range action.Arguments {
|
||||||
|
if strings.HasPrefix(arg.Name, ReservedArgumentNamePrefix) {
|
||||||
|
return fmt.Errorf("action %q argument %q uses reserved prefix %q", action.Title, arg.Name, ReservedArgumentNamePrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) sanitizeDashboardsForInlineActions() {
|
func (cfg *Config) sanitizeDashboardsForInlineActions() {
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,59 @@ func TestSanitizeConfigInlineDashboardActions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateReservedActionArgumentNames(t *testing.T) {
|
||||||
|
c := DefaultConfig()
|
||||||
|
c.Actions = append(c.Actions, &Action{
|
||||||
|
Title: "Reserved arg",
|
||||||
|
Arguments: []ActionArgument{
|
||||||
|
{Name: "ot_custom", Type: "ascii"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
err := c.validateReservedActionArgumentNames()
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `action "Reserved arg" argument "ot_custom" uses reserved prefix "ot_"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateReservedActionArgumentNamesAllowsNonReserved(t *testing.T) {
|
||||||
|
c := DefaultConfig()
|
||||||
|
c.Actions = append(c.Actions, &Action{
|
||||||
|
Title: "Allowed arg",
|
||||||
|
Arguments: []ActionArgument{
|
||||||
|
{Name: "target", Type: "ascii"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, c.validateReservedActionArgumentNames())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateReservedActionArgumentNamesChecksInlineActions(t *testing.T) {
|
||||||
|
c := DefaultConfig()
|
||||||
|
c.Dashboards = []*DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Dashboard",
|
||||||
|
Contents: []*DashboardComponent{
|
||||||
|
{
|
||||||
|
Title: "Inline reserved arg",
|
||||||
|
InlineAction: &Action{
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []ActionArgument{
|
||||||
|
{Name: "ot_custom", Type: "ascii"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sanitizeDashboardsForInlineActions()
|
||||||
|
err := c.validateReservedActionArgumentNames()
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `action "Inline reserved arg" argument "ot_custom" uses reserved prefix "ot_"`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
|
func TestValidateUniqueLocalUserAPIKeys(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ var (
|
||||||
"unicode_identifier": `^[\w\-\.\_\d]+$`,
|
"unicode_identifier": `^[\w\-\.\_\d]+$`,
|
||||||
"ascii": `^[a-zA-Z0-9]+$`,
|
"ascii": `^[a-zA-Z0-9]+$`,
|
||||||
"ascii_identifier": `^[a-zA-Z0-9\-\._]+$`,
|
"ascii_identifier": `^[a-zA-Z0-9\-\._]+$`,
|
||||||
|
"shell_safe_identifier": `^[a-zA-Z0-9@\.\_\+\-]+$`,
|
||||||
"ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`,
|
"ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -576,6 +576,38 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
hasError bool
|
||||||
|
}{
|
||||||
|
{"Simple username", "alice123", false},
|
||||||
|
{"Email username", "alice@example.com", false},
|
||||||
|
{"Plus addressing", "alice+test@example.com", false},
|
||||||
|
{"Hyphen underscore dot", "alice-test_user.example", false},
|
||||||
|
{"Invalid space", "alice example", true},
|
||||||
|
{"Invalid shell substitution", "$(whoami)", true},
|
||||||
|
{"Invalid backtick", "`whoami`", true},
|
||||||
|
{"Invalid semicolon", "alice;id", true},
|
||||||
|
{"Invalid ampersand", "alice&id", true},
|
||||||
|
{"Invalid pipe", "alice|id", true},
|
||||||
|
{"Invalid quote", "alice'example", true},
|
||||||
|
{"Invalid slash", "alice/example", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier")
|
||||||
|
if tt.hasError {
|
||||||
|
assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
|
func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -666,13 +666,15 @@ func stepACLCheck(req *ExecutionRequest) bool {
|
||||||
|
|
||||||
func stepParseArgs(req *ExecutionRequest) bool {
|
func stepParseArgs(req *ExecutionRequest) bool {
|
||||||
ensureArgumentMap(req)
|
ensureArgumentMap(req)
|
||||||
injectSystemArgs(req)
|
|
||||||
|
|
||||||
if !hasBindingAndAction(req) {
|
if !hasBindingAndAction(req) {
|
||||||
return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
|
return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil"))
|
||||||
}
|
}
|
||||||
|
|
||||||
filterToDefinedArgumentsOnly(req)
|
filterToDefinedArgumentsOnly(req)
|
||||||
|
if err := injectSystemArgs(req); err != nil {
|
||||||
|
return fail(req, err)
|
||||||
|
}
|
||||||
mangleInvalidArgumentValues(req)
|
mangleInvalidArgumentValues(req)
|
||||||
|
|
||||||
if hasExec(req) {
|
if hasExec(req) {
|
||||||
|
|
@ -735,7 +737,7 @@ func filterToDefinedArgumentsOnly(req *ExecutionRequest) {
|
||||||
|
|
||||||
func keepArgument(name string, definedNames map[string]struct{}) bool {
|
func keepArgument(name string, definedNames map[string]struct{}) bool {
|
||||||
_, ok := definedNames[name]
|
_, ok := definedNames[name]
|
||||||
return ok || strings.HasPrefix(name, "ot_")
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasWebhookTag(req *ExecutionRequest) bool {
|
func hasWebhookTag(req *ExecutionRequest) bool {
|
||||||
|
|
@ -747,9 +749,38 @@ func hasWebhookTag(req *ExecutionRequest) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func injectSystemArgs(req *ExecutionRequest) {
|
var systemArgumentDefinitions = []config.ActionArgument{
|
||||||
req.Arguments["ot_executionTrackingId"] = req.TrackingID
|
{Name: "ot_executionTrackingId", Type: "ascii_identifier", RejectNull: true},
|
||||||
req.Arguments["ot_username"] = req.AuthenticatedUser.Username
|
{Name: "ot_username", Type: "shell_safe_identifier", RejectNull: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func injectSystemArgs(req *ExecutionRequest) error {
|
||||||
|
args, err := validatedSystemArgs(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range args {
|
||||||
|
req.Arguments[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatedSystemArgs(req *ExecutionRequest) (map[string]string, error) {
|
||||||
|
values := map[string]string{
|
||||||
|
"ot_executionTrackingId": req.TrackingID,
|
||||||
|
"ot_username": req.AuthenticatedUser.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range systemArgumentDefinitions {
|
||||||
|
arg := &systemArgumentDefinitions[i]
|
||||||
|
if err := ValidateArgument(arg, values[arg.Name], req.Binding.Action); err != nil {
|
||||||
|
return nil, fmt.Errorf("system argument %q failed validation: %w", arg.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasBindingAndAction(req *ExecutionRequest) bool {
|
func hasBindingAndAction(req *ExecutionRequest) bool {
|
||||||
|
|
@ -939,36 +970,20 @@ func prepareCommand(cmd *exec.Cmd, streamer *OutputStreamer, req *ExecutionReque
|
||||||
}
|
}
|
||||||
|
|
||||||
func stepExecAfter(req *ExecutionRequest) bool {
|
func stepExecAfter(req *ExecutionRequest) bool {
|
||||||
if req.Binding.Action.ShellAfterCompleted == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor)
|
ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
args := map[string]string{
|
cmd, args, err := buildShellAfterCommand(ctx, req, &stdout, &stderr)
|
||||||
"output": req.logEntry.Output,
|
|
||||||
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
|
|
||||||
"ot_executionTrackingId": req.TrackingID,
|
|
||||||
"ot_username": req.AuthenticatedUser.Username,
|
|
||||||
}
|
|
||||||
|
|
||||||
finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
|
return fail(req, err)
|
||||||
req.logEntry.Output += msg
|
}
|
||||||
log.Warn(msg)
|
if cmd == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := wrapCommandInShell(ctx, finalParsedCommand)
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
cmd.Env = buildEnv(args)
|
cmd.Env = buildEnv(args)
|
||||||
|
|
||||||
runerr := cmd.Start()
|
runerr := cmd.Start()
|
||||||
|
|
@ -998,6 +1013,43 @@ func stepExecAfter(req *ExecutionRequest) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildShellAfterCommand(ctx context.Context, req *ExecutionRequest, stdout, stderr *bytes.Buffer) (*exec.Cmd, map[string]string, error) {
|
||||||
|
if req.Binding.Action.ShellAfterCompleted == "" {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
|
||||||
|
if err != nil {
|
||||||
|
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
|
||||||
|
req.logEntry.Output += msg
|
||||||
|
log.Warn(msg)
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := wrapCommandInShell(ctx, finalParsedCommand)
|
||||||
|
cmd.Stdout = stdout
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
|
||||||
|
return cmd, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildShellAfterArgs(req *ExecutionRequest) (map[string]string, error) {
|
||||||
|
args, err := validatedSystemArgs(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
args["output"] = req.logEntry.Output
|
||||||
|
args["exitCode"] = fmt.Sprintf("%v", req.logEntry.ExitCode)
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
//gocyclo:ignore
|
//gocyclo:ignore
|
||||||
func stepTrigger(req *ExecutionRequest) bool {
|
func stepTrigger(req *ExecutionRequest) bool {
|
||||||
if req.Binding.Action.Triggers == nil {
|
if req.Binding.Action.Triggers == nil {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -379,7 +380,7 @@ func TestFilterToDefinedArgumentsOnly(t *testing.T) {
|
||||||
assert.Empty(t, req.Arguments["extra_undefined"])
|
assert.Empty(t, req.Arguments["extra_undefined"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
|
func TestFilterToDefinedArgumentsDropsReservedPrefixArgs(t *testing.T) {
|
||||||
req := newExecRequest()
|
req := newExecRequest()
|
||||||
req.Binding.Action = &config.Action{
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Filter test",
|
Title: "Filter test",
|
||||||
|
|
@ -393,8 +394,209 @@ func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
|
||||||
|
|
||||||
filterToDefinedArgumentsOnly(req)
|
filterToDefinedArgumentsOnly(req)
|
||||||
|
|
||||||
assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"])
|
assert.Empty(t, req.Arguments["ot_executionTrackingId"])
|
||||||
assert.Equal(t, "webhook", req.Arguments["ot_username"])
|
assert.Empty(t, req.Arguments["ot_username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsInjectsSystemArgsAfterFiltering(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Filter then inject",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{Name: "name", Type: "ascii"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Arguments = map[string]string{
|
||||||
|
"name": "Alice",
|
||||||
|
"ot_executionTrackingId": "attacker-track",
|
||||||
|
"ot_username": "mallory",
|
||||||
|
"ot_custom": "polluted",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, stepParseArgs(req))
|
||||||
|
assert.Equal(t, "Alice", req.Arguments["name"])
|
||||||
|
assert.Equal(t, "server-track-456", req.Arguments["ot_executionTrackingId"])
|
||||||
|
assert.Equal(t, "alice", req.Arguments["ot_username"])
|
||||||
|
assert.Empty(t, req.Arguments["ot_custom"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsDropsReservedPrefixArgsFromEnvironment(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "No reserved prefix pollution",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
req.Arguments = map[string]string{
|
||||||
|
"ot_custom": "polluted",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, stepParseArgs(req))
|
||||||
|
env := buildEnv(req.Arguments)
|
||||||
|
|
||||||
|
assert.False(t, containsEnvPrefix(env, "OT_CUSTOM="))
|
||||||
|
assert.True(t, containsEnvPrefix(env, "OT_USERNAME=alice@example.com"))
|
||||||
|
assert.True(t, containsEnvPrefix(env, "OT_EXECUTIONTRACKINGID=server-track-456"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSystemArgumentDefinitionsAreReservedAndShellSafe(t *testing.T) {
|
||||||
|
unsafeTypes := map[string]struct{}{
|
||||||
|
"email": {},
|
||||||
|
"password": {},
|
||||||
|
"raw_string_multiline": {},
|
||||||
|
"url": {},
|
||||||
|
"very_dangerous_raw_string": {},
|
||||||
|
}
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, arg := range systemArgumentDefinitions {
|
||||||
|
assert.True(t, strings.HasPrefix(arg.Name, config.ReservedArgumentNamePrefix))
|
||||||
|
assert.NotEmpty(t, arg.Type)
|
||||||
|
assert.True(t, arg.RejectNull)
|
||||||
|
|
||||||
|
_, duplicate := seen[arg.Name]
|
||||||
|
assert.False(t, duplicate, "duplicate system argument definition %q", arg.Name)
|
||||||
|
seen[arg.Name] = struct{}{}
|
||||||
|
|
||||||
|
_, unsafe := unsafeTypes[arg.Type]
|
||||||
|
assert.False(t, unsafe, "system argument %q uses unsafe type %q", arg.Name, arg.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatedSystemArgsMatchesSystemArgumentDefinitions(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
|
||||||
|
args, err := validatedSystemArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, args, len(systemArgumentDefinitions))
|
||||||
|
for _, arg := range systemArgumentDefinitions {
|
||||||
|
assert.Contains(t, args, arg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildShellAfterArgsOnlyAddsExpectedNonSystemArgs(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{
|
||||||
|
Output: "hello",
|
||||||
|
ExitCode: 7,
|
||||||
|
}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{ShellAfterCompleted: "echo test"}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, args, len(systemArgumentDefinitions)+2)
|
||||||
|
assert.Contains(t, args, "output")
|
||||||
|
assert.Contains(t, args, "exitCode")
|
||||||
|
for _, arg := range systemArgumentDefinitions {
|
||||||
|
assert.Contains(t, args, arg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsAllowsEmailUsernameSystemArg(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Email username",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, stepParseArgs(req))
|
||||||
|
assert.Equal(t, "alice@example.com", req.Arguments["ot_username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsFailsWhenUsernameSystemArgIsInvalid(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Invalid system arg",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, stepParseArgs(req))
|
||||||
|
assert.Contains(t, req.logEntry.Output, `system argument "ot_username" failed validation`)
|
||||||
|
assert.Empty(t, req.Arguments["ot_username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepParseArgsFailsWhenTrackingIDSystemArgIsInvalid(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "track/../../bad"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Invalid tracking ID",
|
||||||
|
Shell: "echo test",
|
||||||
|
Arguments: []config.ActionArgument{},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, stepParseArgs(req))
|
||||||
|
assert.Contains(t, req.logEntry.Output, `system argument "ot_executionTrackingId" failed validation`)
|
||||||
|
assert.Empty(t, req.Arguments["ot_executionTrackingId"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildShellAfterArgsUsesValidatedSystemArgs(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{
|
||||||
|
Output: "hello",
|
||||||
|
ExitCode: 7,
|
||||||
|
}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Shell after",
|
||||||
|
ShellAfterCompleted: "echo test",
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "alice@example.com", args["ot_username"])
|
||||||
|
assert.Equal(t, "server-track-456", args["ot_executionTrackingId"])
|
||||||
|
assert.Equal(t, "hello", args["output"])
|
||||||
|
assert.Equal(t, "7", args["exitCode"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildShellAfterArgsFailsWhenSystemArgIsInvalid(t *testing.T) {
|
||||||
|
req := newExecRequest()
|
||||||
|
req.logEntry = &InternalLogEntry{}
|
||||||
|
req.TrackingID = "server-track-456"
|
||||||
|
req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"}
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
|
Title: "Shell after invalid username",
|
||||||
|
ShellAfterCompleted: "echo test",
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := buildShellAfterArgs(req)
|
||||||
|
|
||||||
|
assert.Nil(t, args)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `system argument "ot_username" failed validation`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsEnvPrefix(env []string, prefix string) bool {
|
||||||
|
for _, item := range env {
|
||||||
|
if strings.HasPrefix(item, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTriggerExecutesTriggeredAction(t *testing.T) {
|
func TestTriggerExecutesTriggeredAction(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue