diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c79913f..4958aae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,6 +76,44 @@ jobs: - name: go test run: | go test + fuzz: + name: 10s fuzz test + runs-on: ubuntu-24.04 + continue-on-error: true + permissions: + contents: read # Required to clone the repo + steps: + - name: Harden the runner + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + - name: go fuzz + run: | + go test -fuzz=FuzzTemplatefile -fuzztime=10s + - name: Collect any new fuzz corpora + id: corpus + if: failure() + run: | + { + printf '%s\n' 'paths<> "$GITHUB_OUTPUT" + - name: Upload fuzz failure corpora + if: failure() && steps.corpus.outputs.paths != '' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: fuzz-failure-corpus + path: ${{ steps.corpus.outputs.paths }} + retention-days: 14 no-more-jobs: name: No more jobs runs-on: ubuntu-24.04 @@ -83,6 +121,7 @@ jobs: - govulncheck - pinact-verify - fmt-test + - fuzz steps: - name: Harden the runner uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 diff --git a/templatefile.go b/templatefile.go index a9ecb45..d44ee77 100644 --- a/templatefile.go +++ b/templatefile.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/hashicorp/hcl/v2/ext/tryfunc" "github.com/hashicorp/terraform/lang/funcs" ctyyaml "github.com/zclconf/go-cty-yaml" @@ -138,5 +140,8 @@ func templatefile(baseDir string, path string, vars string) (string, error) { return "", err } - return res.AsString(), err + if res.Type() != cty.String { + return "", fmt.Errorf("template result is %s, not string", res.Type().FriendlyName()) + } + return res.AsString(), nil } diff --git a/templatefile_test.go b/templatefile_test.go index 07f1f45..f6f8249 100644 --- a/templatefile_test.go +++ b/templatefile_test.go @@ -2,9 +2,11 @@ package main import ( "io/ioutil" + "os" "path/filepath" "strings" "testing" + "unicode/utf8" "github.com/stretchr/testify/require" ) @@ -33,3 +35,20 @@ func TestTemplatefileRecursive(t *testing.T) { require.NotNil(t, err) require.Contains(t, err.Error(), "Call to unknown function") } + +func FuzzTemplatefile(f *testing.F) { + f.Add("Hello, ${name}!", "name: world\n") + f.Add("${val}", "val: works\n") + f.Add(`The items are ${join(", ", list)}`, "list:\n - foo\n - bar\n") + f.Add("%{ for x in list ~}\n- ${x}\n%{ endfor ~}", "list:\n - foo\n") + + f.Fuzz(func(t *testing.T, tmpl, vars string) { + tmp := t.TempDir() + path := filepath.Join(tmp, "fuzz.tmpl") + require.NoError(t, os.WriteFile(path, []byte(tmpl), 0o600)) + + // Assert that the parser doesn't panic or hang, and produces valid UTF-8 + actual, err := templatefile(tmp, path, vars) + require.True(t, err != nil || utf8.ValidString(actual), "output is invalid UTF-8") + }) +} diff --git a/testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 b/testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 new file mode 100644 index 0000000..06d9461 --- /dev/null +++ b/testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 @@ -0,0 +1,3 @@ +go test fuzz v1 +string("${val}") +string("val:")