cli: Prevent overuse of console-only type function

The console-only `type` function allows interrogation of any value's
type.  An implementation quirk is that we use a cty.Mark to allow the
console to display this type information without the usual HCL quoting.
For example:

> type("boop")
string

instead of:

> type("boop")
"string"

Because these marks can propagate when used in complex expressions,
using the type function as part of a complex expression could result in
this "print as raw" mark being attached to a collection. When this
happened, it would result in a crash when we tried to iterate over a
marked value.

The `type` function was never intended to be used in this way, which is
why its use is limited to the console command. Its purpose was as a
pseudo-builtin, used only at the top level to display the type of a
given value.

This commit goes some way to preventing the use of the `type` function
in complex expressions, by refusing to display any non-string value
which was marked by `type`, or contains a sub-value which was so marked.
This commit is contained in:
Alisdair McDiarmid 2022-02-04 10:32:06 -05:00
parent c1dc94a3d2
commit 691b98b612
3 changed files with 53 additions and 4 deletions

View File

@ -17,10 +17,6 @@ func FormatValue(v cty.Value, indent int) string {
if !v.IsKnown() {
return "(known after apply)"
}
if v.Type().Equals(cty.String) && v.HasMark(marks.Raw) {
raw, _ := v.Unmark()
return raw.AsString()
}
if v.HasMark(marks.Sensitive) {
return "(sensitive)"
}

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/tfdiags"
)
@ -54,6 +55,29 @@ func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) {
return "", diags
}
// The raw mark is used only by the console-only `type` function, in order
// to allow display of a string value representation of the type without the
// usual HCL formatting. If we receive a string value with this mark, we do
// not want to format it any further.
//
// Due to mark propagation in cty, calling `type` as part of a larger
// expression can lead to other values being marked, which can in turn lead
// to unpredictable results. If any non-string value has the raw mark, we
// return a diagnostic explaining that this use of `type` is not permitted.
if marks.Contains(val, marks.Raw) {
if val.Type().Equals(cty.String) {
raw, _ := val.Unmark()
return raw.AsString(), diags
} else {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid use of type function",
"The console-only \"type\" function cannot be used as part of an expression.",
))
return "", diags
}
}
return FormatValue(val, 0), diags
}

View File

@ -120,6 +120,20 @@ func TestSession_basicState(t *testing.T) {
},
})
})
t.Run("type function", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "type(test_instance.foo)",
Output: `object({
id: string,
})`,
},
},
})
})
}
func TestSession_stateless(t *testing.T) {
@ -178,6 +192,18 @@ func TestSession_stateless(t *testing.T) {
},
})
})
t.Run("type function cannot be used in expressions", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: `[for i in [1, "two", true]: type(i)]`,
Error: true,
ErrorContains: "Invalid use of type function",
},
},
})
})
}
func testSession(t *testing.T, test testSessionTest) {
@ -221,6 +247,9 @@ func testSession(t *testing.T, test testSessionTest) {
t.Fatalf("failed to create scope: %s", diags.Err())
}
// Ensure that any console-only functions are available
scope.ConsoleMode = true
// Build the session
s := &Session{
Scope: scope,