Ctrl K
ComunidadGuías…

pkg/clientgen/quote_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package clientgen

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	qt "github.com/frankban/quicktest"
	"github.com/rogpeppe/go-internal/txtar"

	"encr.dev/cli/daemon/apps"
	"encr.dev/pkg/builder"
	"encr.dev/pkg/clientgen/clientgentypes"
	"encr.dev/v2/v2builder"
)

// TestQuoteJSString verifies that values which would otherwise break out of a
// JS/TS string literal are escaped.
func TestQuoteJSString(t *testing.T) {
	c := qt.New(t)
	cases := []struct {
		in   string
		want string
	}{
		{`plain`, `"plain"`},
		{`x-token`, `"x-token"`},
		{`a"b`, `"a\"b"`},
		{`a\b`, `"a\\b"`},
		{"line\nbreak", `"line\nbreak"`},
		{"tab\there", `"tab\there"`},
		// The header-name breakout payload from the vulnerability report.
		{`x");evil();("`, `"x\");evil();(\""`},
	}
	for _, tc := range cases {
		c.Assert(quoteJSString(tc.in), qt.Equals, tc.want, qt.Commentf("input %q", tc.in))
		// The result must never contain an unescaped double-quote in its interior.
		inner := quoteJSString(tc.in)
		inner = inner[1 : len(inner)-1]
		c.Assert(strings.Contains(strings.ReplaceAll(inner, `\"`, ""), `"`), qt.IsFalse)
	}
}

// TestEscapeDocComment verifies that a doc comment cannot terminate the
// enclosing block comment.
func TestEscapeDocComment(t *testing.T) {
	c := qt.New(t)
	c.Assert(strings.Contains(escapeDocComment(`ok */ evil()`), `*/`), qt.IsFalse)
	c.Assert(strings.Contains(escapeDocComment(`**/`), `*/`), qt.IsFalse)
	c.Assert(escapeDocComment(`no terminator here`), qt.Equals, `no terminator here`)
}

// TestClientGenInjectionNeutralized reproduces the code-injection vectors from
// the vulnerability report against the real generator and asserts the emitted
// client is neither syntactically broken nor executes injected code on import.
func TestClientGenInjectionNeutralized(t *testing.T) {
	c := qt.New(t)

	// The service (package) doc breaks out of a /** */ block comment; the
	// response header wire name breaks out of a string literal.
	src := `-- go.mod --
module app

require (
	encore.dev v1.52.1
)

-- encore.app --
{"id": ""}

-- evil/evil.go --
// Package evil is documented.
// */ ;import("node:fs").then(f=>f.writeFileSync(process.env.CLIENTGEN_MARKER,"pwned")); /*
package evil

import "context"

// Evil is a normal endpoint.
// */ ;import("node:fs").then(f=>f.writeFileSync(process.env.CLIENTGEN_MARKER,"pwned")); /*
type Response struct {
	Token string ` + "`header:\"x\\\");import(process.env.clientgen_marker);((\\\"\"`" + `
}

//encore:api public method=GET path=/evil
func Evil(ctx context.Context) (*Response, error) {
	return &Response{}, nil
}
`

	base := t.TempDir()
	c.Assert(txtar.Write(txtar.Parse([]byte(src)), base), qt.IsNil)

	ctx := context.Background()
	bld := v2builder.New()
	app := apps.NewInstance(base, "app", "")
	prepareResult, err := bld.Prepare(ctx, builder.PrepareParams{
		Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".",
	})
	c.Assert(err, qt.IsNil)
	res, err := bld.Parse(ctx, builder.ParseParams{
		Build: builder.DefaultBuildInfo(), App: app, WorkingDir: ".", Prepare: prepareResult,
	})
	c.Assert(err, qt.IsNil)

	services := clientgentypes.AllServices(res.Meta)

	genJS, err := Client(LangJavascript, "app", res.Meta, services, clientgentypes.TagSet{}, clientgentypes.Options{})
	c.Assert(err, qt.IsNil)
	genTS, err := Client(LangTypeScript, "app", res.Meta, services, clientgentypes.TagSet{}, clientgentypes.Options{})
	c.Assert(err, qt.IsNil)

	// The doc payload must be neutralized: no "*/" that would close the comment.
	// (The escaped form "*\/" is what should appear instead.)
	c.Assert(strings.Contains(string(genJS), `*/ ;import`), qt.IsFalse)
	c.Assert(strings.Contains(string(genTS), `*/ ;import`), qt.IsFalse)

	node, lookErr := exec.LookPath("node")
	if lookErr != nil {
		t.Skip("node not available; skipping runtime checks")
	}

	jsPath := filepath.Join(base, "client.mjs")
	c.Assert(os.WriteFile(jsPath, genJS, 0o644), qt.IsNil)

	// 1. The generated client must be syntactically valid JS. Before the fix the
	//    header breakout produced "SyntaxError: missing ) after argument list".
	checkOut, err := exec.Command(node, "--check", jsPath).CombinedOutput()
	c.Assert(err, qt.IsNil, qt.Commentf("node --check failed: %s", checkOut))

	// 2. Merely importing the client must not execute injected code. The payloads
	//    would write CLIENTGEN_MARKER; assert the file is never created.
	marker := filepath.Join(base, "MARKER")
	runCmd := exec.Command(node, "--input-type=module", "-e",
		"await import(process.argv[1]); await new Promise(r=>setTimeout(r,200));", jsPath)
	runCmd.Env = append(os.Environ(), "CLIENTGEN_MARKER="+marker, "clientgen_marker="+marker)
	runOut, _ := runCmd.CombinedOutput()
	_, statErr := os.Stat(marker)
	c.Assert(os.IsNotExist(statErr), qt.IsTrue,
		qt.Commentf("injected payload executed on import; node output: %s", runOut))
}