diff --git a/lib/web/join_tokens.go b/lib/web/join_tokens.go index a00f1bbcac9a1..06dac918f902c 100644 --- a/lib/web/join_tokens.go +++ b/lib/web/join_tokens.go @@ -603,6 +603,10 @@ func getJoinScript(ctx context.Context, settings scriptSettings, m nodeAPIGetter labelsList := []string{} for labelKey, labelValues := range token.GetSuggestedLabels() { + labelKey = shsprintf.EscapeDefaultContext(labelKey) + for i := range labelValues { + labelValues[i] = shsprintf.EscapeDefaultContext(labelValues[i]) + } labels := strings.Join(labelValues, " ") labelsList = append(labelsList, fmt.Sprintf("%s=%s", labelKey, labels)) } @@ -660,7 +664,7 @@ func getJoinScript(ctx context.Context, settings scriptSettings, m nodeAPIGetter // This section relies on Go's default zero values to make sure that the settings // are correct when not installing an app. err = scripts.InstallNodeBashScript.Execute(&buf, map[string]interface{}{ - "token": settings.token, + "token": shsprintf.EscapeDefaultContext(settings.token), "hostname": hostname, "port": portStr, // The install.sh script has some manually generated configs and some diff --git a/lib/web/join_tokens_test.go b/lib/web/join_tokens_test.go index 08be47c4e448c..05530181ba2de 100644 --- a/lib/web/join_tokens_test.go +++ b/lib/web/join_tokens_test.go @@ -603,6 +603,7 @@ func toHex(s string) string { return hex.EncodeToString([]byte(s)) } func TestGetNodeJoinScript(t *testing.T) { validToken := "f18da1c9f6630a51e8daf121e7451daa" + validTokenWithLabelsWithSpecialChars := "f18da1c9f6630a51e8daf121e7451dbb" validIAMToken := "valid-iam-token" internalResourceID := "967d38ff-7a61-4f42-bd2d-c61965b44db0" @@ -618,19 +619,25 @@ func TestGetNodeJoinScript(t *testing.T) { return &proto.GetClusterCACertResponse{TLSCA: fakeBytes}, nil }, mockGetToken: func(_ context.Context, token string) (types.ProvisionToken, error) { - if token == validToken || token == validIAMToken { - return &types.ProvisionTokenV2{ - Metadata: types.Metadata{ - Name: token, - }, - Spec: types.ProvisionTokenSpecV2{ - SuggestedLabels: types.Labels{ - types.InternalResourceIDLabel: utils.Strings{internalResourceID}, - }, + baseToken := &types.ProvisionTokenV2{ + Metadata: types.Metadata{ + Name: token, + }, + Spec: types.ProvisionTokenSpecV2{ + SuggestedLabels: types.Labels{ + types.InternalResourceIDLabel: utils.Strings{internalResourceID}, }, - }, nil + }, } - return nil, trace.NotFound("token does not exist") + switch token { + case validToken, validIAMToken: + case validTokenWithLabelsWithSpecialChars: + baseToken.Spec.SuggestedLabels["env"] = []string{"bad label value | ; & $ > < ' !"} + baseToken.Spec.SuggestedLabels["bad label key | ; & $ > < ' !"] = []string{"env"} + default: + return nil, trace.NotFound("token does not exist") + } + return baseToken, nil }, } @@ -638,7 +645,7 @@ func TestGetNodeJoinScript(t *testing.T) { desc string settings scriptSettings errAssert require.ErrorAssertionFunc - extraAssertions func(script string) + extraAssertions func(t *testing.T, script string) }{ { desc: "zero value", @@ -659,7 +666,7 @@ func TestGetNodeJoinScript(t *testing.T) { desc: "valid", settings: scriptSettings{token: validToken}, errAssert: require.NoError, - extraAssertions: func(script string) { + extraAssertions: func(t *testing.T, script string) { require.Contains(t, script, validToken) require.Contains(t, script, "test-host") require.Contains(t, script, "12345678") @@ -682,7 +689,7 @@ func TestGetNodeJoinScript(t *testing.T) { joinMethod: string(types.JoinMethodIAM), }, errAssert: require.NoError, - extraAssertions: func(script string) { + extraAssertions: func(t *testing.T, script string) { require.Contains(t, script, "JOIN_METHOD='iam'") }, }, @@ -690,11 +697,20 @@ func TestGetNodeJoinScript(t *testing.T) { desc: "internal resourceid label", settings: scriptSettings{token: validToken}, errAssert: require.NoError, - extraAssertions: func(script string) { + extraAssertions: func(t *testing.T, script string) { require.Contains(t, script, "--labels ") require.Contains(t, script, fmt.Sprintf("%s=%s", types.InternalResourceIDLabel, internalResourceID)) }, }, + { + desc: "attempt to shell injection using suggested labels", + settings: scriptSettings{token: validTokenWithLabelsWithSpecialChars}, + errAssert: require.NoError, + extraAssertions: func(t *testing.T, script string) { + require.Contains(t, script, `bad\ label\ key\ \|\ \;\ \&\ \$\ \>\ \<\ \'\ \!=env`) + require.Contains(t, script, `env=bad\ label\ value\ \|\ \;\ \&\ \$\ \>\ \<\ \'\ \!`) + }, + }, } { t.Run(test.desc, func(t *testing.T) { script, err := getJoinScript(context.Background(), test.settings, m) @@ -704,7 +720,7 @@ func TestGetNodeJoinScript(t *testing.T) { } if test.extraAssertions != nil { - test.extraAssertions(script) + test.extraAssertions(t, script) } }) } @@ -899,6 +915,8 @@ func TestGetAppJoinScript(t *testing.T) { func TestGetDatabaseJoinScript(t *testing.T) { validToken := "f18da1c9f6630a51e8daf121e7451daa" emptySuggestedAgentMatcherLabelsToken := "f18da1c9f6630a51e8daf121e7451000" + wildcardLabelMatcherToken := "f18da1c9f6630a51e8daf121e7451001" + tokenWithSpecialChars := "f18da1c9f6630a51e8daf121e7451002" internalResourceID := "967d38ff-7a61-4f42-bd2d-c61965b44db0" m := &mockedNodeAPIGetter{ @@ -935,6 +953,22 @@ func TestGetDatabaseJoinScript(t *testing.T) { provisionToken.Spec.SuggestedAgentMatcherLabels = types.Labels{} return provisionToken, nil } + if token == wildcardLabelMatcherToken { + provisionToken.Spec.SuggestedAgentMatcherLabels = types.Labels{"*": []string{"*"}} + return provisionToken, nil + } + if token == tokenWithSpecialChars { + provisionToken.Spec.SuggestedAgentMatcherLabels = types.Labels{ + "*": utils.Strings{"*"}, + "spa ces": utils.Strings{"spa ces"}, + "EOF": utils.Strings{"test heredoc"}, + `"EOF"`: utils.Strings{"test quoted heredoc"}, + "#'; <>\\#": utils.Strings{"try to escape yaml"}, + "&<>'\"$A,./;'BCD ${ABCD}": utils.Strings{"key with special characters"}, + "value with special characters": utils.Strings{"&<>'\"$A,./;'BCD ${ABCD}", "#&<>'\"$A,./;'BCD ${ABCD}"}, + } + return provisionToken, nil + } return nil, trace.NotFound("token does not exist") }, } @@ -943,7 +977,7 @@ func TestGetDatabaseJoinScript(t *testing.T) { desc string settings scriptSettings errAssert require.ErrorAssertionFunc - extraAssertions func(script string) + extraAssertions func(t *testing.T, script string) }{ { desc: "two installation methods", @@ -961,22 +995,65 @@ func TestGetDatabaseJoinScript(t *testing.T) { token: validToken, }, errAssert: require.NoError, - extraAssertions: func(script string) { + extraAssertions: func(t *testing.T, script string) { require.Contains(t, script, validToken) require.Contains(t, script, "test-host") require.Contains(t, script, "sha256:") require.Contains(t, script, "--labels ") require.Contains(t, script, fmt.Sprintf("%s=%s", types.InternalResourceIDLabel, internalResourceID)) require.Contains(t, script, ` -db_service: - enabled: "yes" - resources: - labels: env: prod os: - mac - linux product: '*' +`) + }, + }, + { + desc: "discover flow with wildcard label matcher", + settings: scriptSettings{ + databaseInstallMode: true, + token: wildcardLabelMatcherToken, + }, + errAssert: require.NoError, + extraAssertions: func(t *testing.T, script string) { + require.Contains(t, script, wildcardLabelMatcherToken) + require.Contains(t, script, "test-host") + require.Contains(t, script, "sha256:") + require.Contains(t, script, "--labels ") + require.Contains(t, script, fmt.Sprintf("%s=%s", types.InternalResourceIDLabel, internalResourceID)) + require.Contains(t, script, ` + - labels: + '*': '*' +`) + }, + }, + { + desc: "discover flow with shell injection attempt in resource matcher labels", + settings: scriptSettings{ + databaseInstallMode: true, + token: tokenWithSpecialChars, + }, + errAssert: require.NoError, + extraAssertions: func(t *testing.T, script string) { + require.Contains(t, script, tokenWithSpecialChars) + require.Contains(t, script, "test-host") + require.Contains(t, script, "sha256:") + require.Contains(t, script, "--labels ") + require.Contains(t, script, fmt.Sprintf("%s=%s", types.InternalResourceIDLabel, internalResourceID)) + require.Contains(t, script, ` + - labels: + '"EOF"': test quoted heredoc + '#''; <>\#': try to escape yaml + '&<>''"$A,./;''BCD ${ABCD}': key with special characters + '*': '*' + EOF: test heredoc + spa ces: spa ces + value with special characters: + - '&<>''"$A,./;''BCD ${ABCD}' + - '#&<>''"$A,./;''BCD ${ABCD}' `) }, }, @@ -987,16 +1064,13 @@ db_service: token: emptySuggestedAgentMatcherLabelsToken, }, errAssert: require.NoError, - extraAssertions: func(script string) { + extraAssertions: func(t *testing.T, script string) { require.Contains(t, script, emptySuggestedAgentMatcherLabelsToken) require.Contains(t, script, "test-host") require.Contains(t, script, "sha256:") require.Contains(t, script, "--labels ") require.Contains(t, script, fmt.Sprintf("%s=%s", types.InternalResourceIDLabel, internalResourceID)) require.Contains(t, script, ` -db_service: - enabled: "yes" - resources: - labels: {} `) @@ -1011,7 +1085,7 @@ db_service: } if test.extraAssertions != nil { - test.extraAssertions(script) + test.extraAssertions(t, script) } }) } diff --git a/lib/web/scripts/node-join/install.sh b/lib/web/scripts/node-join/install.sh index ee209fa396dfd..822b9f42cdeef 100755 --- a/lib/web/scripts/node-join/install.sh +++ b/lib/web/scripts/node-join/install.sh @@ -47,7 +47,7 @@ JOIN_METHOD_FLAG="" [ -n "$JOIN_METHOD" ] && JOIN_METHOD_FLAG="--join-method ${JOIN_METHOD}" # inject labels into the configuration -LABELS='{{.labels}}' +LABELS="{{.labels}}" LABELS_FLAG=() [ -n "$LABELS" ] && LABELS_FLAG=(--labels "${LABELS}") @@ -494,6 +494,10 @@ proxy_service: db_service: enabled: "yes" resources: +EOF + + # Quoting the EOF heredoc indicates to shell to treat this as a literal string and does not try to interpolate or execute anything. + cat << "EOF" >> ${TELEPORT_CONFIG_PATH} - labels:{{range $index, $line := .db_service_resource_labels}} {{$line -}} {{end}}