diff --git a/config/allconfig/load.go b/config/allconfig/load.go index ad090d60d1a..eca9d06dfa9 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -34,6 +34,7 @@ import ( hglob "github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/parser/metadecoders" + "github.com/gohugoio/hugo/tpl" "github.com/spf13/afero" ) @@ -89,6 +90,9 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { return nil, fmt.Errorf("failed to init config: %w", err) } + // This is unfortunate, but this is a global setting. + tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl) + return configs, nil } diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go index 8bd12af4b77..5d0db2fb940 100644 --- a/config/security/securityConfig.go +++ b/config/security/securityConfig.go @@ -68,6 +68,9 @@ type Config struct { // Allow inline shortcodes EnableInlineShortcodes bool `json:"enableInlineShortcodes"` + + // Go templates related security config. + GoTemplates GoTemplates `json:"goTemplates"` } // Exec holds os/exec policies. @@ -93,6 +96,15 @@ type HTTP struct { MediaTypes Whitelist `json:"mediaTypes"` } +type GoTemplates struct { + + // Enable to allow template actions inside bakcticks in ES6 template literals. + // This was blocked in Hugo 0.114.0 for security reasons and you now get errors on the form + // "... appears in a JS template literal" if you have this in your templates. + // See https://github.com/golang/go/issues/59234 + AllowActionJSTmpl bool +} + // ToTOML converts c to TOML with [security] as the root. func (c Config) ToTOML() string { sec := c.ToSecurityMap() diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go index 3bfd59ce33b..12ce3aae449 100644 --- a/config/security/securityConfig_test.go +++ b/config/security/securityConfig_test.go @@ -140,7 +140,7 @@ func TestToTOML(t *testing.T) { got := DefaultConfig.ToTOML() c.Assert(got, qt.Equals, - "[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']", + "[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.goTemplates]\n AllowActionJSTmpl = false\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']", ) } diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go index 146a95d035c..9f592b57f5a 100644 --- a/tpl/internal/go_templates/htmltemplate/context.go +++ b/tpl/internal/go_templates/htmltemplate/context.go @@ -121,6 +121,8 @@ const ( stateJSDqStr // stateJSSqStr occurs inside a JavaScript single quoted string. stateJSSqStr + // stateJSBqStr occurs inside a JavaScript back quoted string. + stateJSBqStr // stateJSRegexp occurs inside a JavaScript regexp literal. stateJSRegexp // stateJSBlockCmt occurs inside a JavaScript /* block comment */. diff --git a/tpl/internal/go_templates/htmltemplate/css.go b/tpl/internal/go_templates/htmltemplate/css.go index 890a0c6b227..f650d8b3e84 100644 --- a/tpl/internal/go_templates/htmltemplate/css.go +++ b/tpl/internal/go_templates/htmltemplate/css.go @@ -238,7 +238,7 @@ func cssValueFilter(args ...any) string { // inside a string that might embed JavaScript source. for i, c := range b { switch c { - case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}': + case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}', '<', '>': return filterFailsafe case '-': // Disallow . diff --git a/tpl/internal/go_templates/htmltemplate/css_test.go b/tpl/internal/go_templates/htmltemplate/css_test.go index 7d8ad8b5989..f44568930dd 100644 --- a/tpl/internal/go_templates/htmltemplate/css_test.go +++ b/tpl/internal/go_templates/htmltemplate/css_test.go @@ -234,6 +234,8 @@ func TestCSSValueFilter(t *testing.T) { {`-exp\000052 ession(alert(1337))`, "ZgotmplZ"}, {`-expre\0000073sion`, "-expre\x073sion"}, {`@import url evil.css`, "ZgotmplZ"}, + {"<", "ZgotmplZ"}, + {">", "ZgotmplZ"}, } for _, test := range tests { got := cssValueFilter(test.css) diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go index 8422b49214b..98b5658f402 100644 --- a/tpl/internal/go_templates/htmltemplate/doc.go +++ b/tpl/internal/go_templates/htmltemplate/doc.go @@ -231,5 +231,12 @@ Least Surprise Property: "A developer (or code reviewer) familiar with HTML, CSS, and JavaScript, who knows that contextual autoescaping happens should be able to look at a {{.}} and correctly infer what sanitization happens." + +As a consequence of the Least Surprise Property, template actions within an +ECMAScript 6 template literal are disabled by default. +Handling string interpolation within these literals is rather complex resulting +in no clear safe way to support it. +To re-enable template actions within ECMAScript 6 template literals, use the +GODEBUG=jstmpllitinterp=1 environment variable. */ package template diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go index 916b41a82c5..0a62563cf4f 100644 --- a/tpl/internal/go_templates/htmltemplate/error.go +++ b/tpl/internal/go_templates/htmltemplate/error.go @@ -215,6 +215,19 @@ const ( // pipeline occurs in an unquoted attribute value context, "html" is // disallowed. Avoid using "html" and "urlquery" entirely in new templates. ErrPredefinedEscaper + + // errJSTmplLit: "... appears in a JS template literal" + // Example: + // + // Discussion: + // Package html/template does not support actions inside of JS template + // literals. + // + // TODO(rolandshoemaker): we cannot add this as an exported error in a minor + // release, since it is backwards incompatible with the other minor + // releases. As such we need to leave it unexported, and then we'll add it + // in the next major release. + errJSTmplLit ) func (e *Error) Error() string { diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go index 3aac865ef1b..ba9b4bbb8db 100644 --- a/tpl/internal/go_templates/htmltemplate/escape.go +++ b/tpl/internal/go_templates/htmltemplate/escape.go @@ -161,6 +161,8 @@ func (e *escaper) escape(c context, n parse.Node) context { panic("escaping " + n.String() + " is unimplemented") } +// var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp") + // escapeAction escapes an action template node. func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { if len(n.Pipe.Decl) != 0 { @@ -224,6 +226,15 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { c.jsCtx = jsCtxDivOp case stateJSDqStr, stateJSSqStr: s = append(s, "_html_template_jsstrescaper") + case stateJSBqStr: + if SecurityAllowActionJSTmpl.Load() { // .Value() == "1" { + s = append(s, "_html_template_jsstrescaper") + } else { + return context{ + state: stateError, + err: errorf(errJSTmplLit, n, n.Line, "%s appears in a JS template literal", n), + } + } case stateJSRegexp: s = append(s, "_html_template_jsregexpescaper") case stateCSS: @@ -370,9 +381,8 @@ func normalizeEscFn(e string) string { // for all x. var redundantFuncs = map[string]map[string]bool{ "_html_template_commentescaper": { - "_html_template_attrescaper": true, - "_html_template_nospaceescaper": true, - "_html_template_htmlescaper": true, + "_html_template_attrescaper": true, + "_html_template_htmlescaper": true, }, "_html_template_cssescaper": { "_html_template_attrescaper": true, diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go index a08ea57ef70..680ba6fa714 100644 --- a/tpl/internal/go_templates/htmltemplate/escape_test.go +++ b/tpl/internal/go_templates/htmltemplate/escape_test.go @@ -683,38 +683,49 @@ func TestEscape(t *testing.T) { ``, ``, }, + { + "unquoted empty attribute value (plaintext)", + "
", + "
", + }, + { + "unquoted empty attribute value (url)", + "
", + "
", + }, + { + "quoted empty attribute value", + "
", + "
", + }, } for _, test := range tests { - tmpl := New(test.name) - tmpl = Must(tmpl.Parse(test.input)) - // Check for bug 6459: Tree field was not set in Parse. - if tmpl.Tree != tmpl.text.Tree { - t.Errorf("%s: tree not set properly", test.name) - continue - } - b := new(strings.Builder) - if err := tmpl.Execute(b, data); err != nil { - t.Errorf("%s: template execution failed: %s", test.name, err) - continue - } - if w, g := test.output, b.String(); w != g { - t.Errorf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) - continue - } - b.Reset() - if err := tmpl.Execute(b, pdata); err != nil { - t.Errorf("%s: template execution failed for pointer: %s", test.name, err) - continue - } - if w, g := test.output, b.String(); w != g { - t.Errorf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) - continue - } - if tmpl.Tree != tmpl.text.Tree { - t.Errorf("%s: tree mismatch", test.name) - continue - } + t.Run(test.name, func(t *testing.T) { + tmpl := New(test.name) + tmpl = Must(tmpl.Parse(test.input)) + // Check for bug 6459: Tree field was not set in Parse. + if tmpl.Tree != tmpl.text.Tree { + t.Fatalf("%s: tree not set properly", test.name) + } + b := new(strings.Builder) + if err := tmpl.Execute(b, data); err != nil { + t.Fatalf("%s: template execution failed: %s", test.name, err) + } + if w, g := test.output, b.String(); w != g { + t.Fatalf("%s: escaped output: want\n\t%q\ngot\n\t%q", test.name, w, g) + } + b.Reset() + if err := tmpl.Execute(b, pdata); err != nil { + t.Fatalf("%s: template execution failed for pointer: %s", test.name, err) + } + if w, g := test.output, b.String(); w != g { + t.Fatalf("%s: escaped output for pointer: want\n\t%q\ngot\n\t%q", test.name, w, g) + } + if tmpl.Tree != tmpl.text.Tree { + t.Fatalf("%s: tree mismatch", test.name) + } + }) } } @@ -941,6 +952,10 @@ func TestErrors(t *testing.T) { "{{range .Items}}{{if .X}}{{break}}{{end}}{{end}}", "", }, + { + "`", + "", + }, // Error cases. { "{{if .Cond}}var tmpl = `asd {{.}}`;", + `{{.}} appears in a JS template literal`, + }, } for _, test := range tests { buf := new(bytes.Buffer) @@ -1308,6 +1327,10 @@ func TestEscapeText(t *testing.T) { `= state(len(_state_index)-1) { diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go index 06df679330d..92eb3519063 100644 --- a/tpl/internal/go_templates/htmltemplate/transition.go +++ b/tpl/internal/go_templates/htmltemplate/transition.go @@ -27,6 +27,7 @@ var transitionFunc = [...]func(context, []byte) (context, int){ stateJS: tJS, stateJSDqStr: tJSDelimited, stateJSSqStr: tJSDelimited, + stateJSBqStr: tJSDelimited, stateJSRegexp: tJSDelimited, stateJSBlockCmt: tBlockCmt, stateJSLineCmt: tLineCmt, @@ -262,7 +263,7 @@ func tURL(c context, s []byte) (context, int) { // tJS is the context transition function for the JS state. func tJS(c context, s []byte) (context, int) { - i := bytes.IndexAny(s, `"'/`) + i := bytes.IndexAny(s, "\"`'/") if i == -1 { // Entire input is non string, comment, regexp tokens. c.jsCtx = nextJSCtx(s, c.jsCtx) @@ -274,6 +275,8 @@ func tJS(c context, s []byte) (context, int) { c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp case '\'': c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp + case '`': + c.state, c.jsCtx = stateJSBqStr, jsCtxRegexp case '/': switch { case i+1 < len(s) && s[i+1] == '/': @@ -303,6 +306,8 @@ func tJSDelimited(c context, s []byte) (context, int) { switch c.state { case stateJSSqStr: specials = `\'` + case stateJSBqStr: + specials = "`\\" case stateJSRegexp: specials = `\/[]` } diff --git a/tpl/internal/go_templates/testenv/exec.go b/tpl/internal/go_templates/testenv/exec.go index ca4023647d4..13b4c7102e7 100644 --- a/tpl/internal/go_templates/testenv/exec.go +++ b/tpl/internal/go_templates/testenv/exec.go @@ -9,11 +9,9 @@ import ( "os" "os/exec" "runtime" - "strconv" "strings" "sync" "testing" - "time" ) // HasExec reports whether the current system can start new processes @@ -84,87 +82,7 @@ func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { // - fails the test if the command does not complete before the test's deadline, and // - sets a Cleanup function that verifies that the test did not leak a subprocess. func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { - t.Helper() - MustHaveExec(t) - - var ( - cancelCtx context.CancelFunc - gracePeriod time.Duration // unlimited unless the test has a deadline (to allow for interactive debugging) - ) - - if t, ok := t.(interface { - testing.TB - Deadline() (time.Time, bool) - }); ok { - if td, ok := t.Deadline(); ok { - // Start with a minimum grace period, just long enough to consume the - // output of a reasonable program after it terminates. - gracePeriod = 100 * time.Millisecond - if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { - scale, err := strconv.Atoi(s) - if err != nil { - t.Fatalf("invalid GO_TEST_TIMEOUT_SCALE: %v", err) - } - gracePeriod *= time.Duration(scale) - } - - // If time allows, increase the termination grace period to 5% of the - // test's remaining time. - testTimeout := time.Until(td) - if gp := testTimeout / 20; gp > gracePeriod { - gracePeriod = gp - } - - // When we run commands that execute subprocesses, we want to reserve two - // grace periods to clean up: one for the delay between the first - // termination signal being sent (via the Cancel callback when the Context - // expires) and the process being forcibly terminated (via the WaitDelay - // field), and a second one for the delay becween the process being - // terminated and and the test logging its output for debugging. - // - // (We want to ensure that the test process itself has enough time to - // log the output before it is also terminated.) - cmdTimeout := testTimeout - 2*gracePeriod - - if cd, ok := ctx.Deadline(); !ok || time.Until(cd) > cmdTimeout { - // Either ctx doesn't have a deadline, or its deadline would expire - // after (or too close before) the test has already timed out. - // Add a shorter timeout so that the test will produce useful output. - ctx, cancelCtx = context.WithTimeout(ctx, cmdTimeout) - } - } - } - - cmd := exec.CommandContext(ctx, name, args...) - /*cmd.Cancel = func() error { - if cancelCtx != nil && ctx.Err() == context.DeadlineExceeded { - // The command timed out due to running too close to the test's deadline. - // There is no way the test did that intentionally — it's too close to the - // wire! — so mark it as a test failure. That way, if the test expects the - // command to fail for some other reason, it doesn't have to distinguish - // between that reason and a timeout. - t.Errorf("test timed out while running command: %v", cmd) - } else { - // The command is being terminated due to ctx being canceled, but - // apparently not due to an explicit test deadline that we added. - // Log that information in case it is useful for diagnosing a failure, - // but don't actually fail the test because of it. - t.Logf("%v: terminating command: %v", ctx.Err(), cmd) - } - return cmd.Process.Signal(Sigquit) - } - cmd.WaitDelay = gracePeriod*/ - - t.Cleanup(func() { - if cancelCtx != nil { - cancelCtx() - } - if cmd.Process != nil && cmd.ProcessState == nil { - t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) - } - }) - - return cmd + panic("Not implemented, Hugo is not using this") } // Command is like exec.Command, but applies the same changes as diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go index 6bfa54a979c..91de6e76cbb 100644 --- a/tpl/internal/go_templates/testenv/testenv.go +++ b/tpl/internal/go_templates/testenv/testenv.go @@ -258,7 +258,7 @@ func MustHaveCGO(t testing.TB) { // CanInternalLink reports whether the current system can link programs with // internal linking. func CanInternalLink() bool { - return false + panic("not implemented, not needed by Hugo") } // MustInternalLink checks that the current system can link programs with internal @@ -349,15 +349,5 @@ func SkipIfOptimizationOff(t testing.TB) { // dstPath containing entries for the packages in std and cmd in addition // to the package to package file mappings in additionalPackageFiles. func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) { - /*importcfg, err := goroot.Importcfg() - for k, v := range additionalPackageFiles { - importcfg += fmt.Sprintf("\npackagefile %s=%s", k, v) - } - if err != nil { - t.Fatalf("preparing the importcfg failed: %s", err) - } - err = os.WriteFile(dstPath, []byte(importcfg), 0655) - if err != nil { - t.Fatalf("writing the importcfg failed: %s", err) - }*/ + panic("not implemented, not needed by Hugo") } diff --git a/tpl/internal/go_templates/testenv/testenv_test.go b/tpl/internal/go_templates/testenv/testenv_test.go index 97c92a6e949..2f72080c2b0 100644 --- a/tpl/internal/go_templates/testenv/testenv_test.go +++ b/tpl/internal/go_templates/testenv/testenv_test.go @@ -13,7 +13,8 @@ import ( "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv" ) -func _TestGoToolLocation(t *testing.T) { +func TestGoToolLocation(t *testing.T) { + t.Skip("skipping test that requires go command") testenv.MustHaveGoBuild(t) var exeSuffix string diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index 62b2e519ea0..597866c6866 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -361,19 +361,27 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { // mark top of stack before any variables in the body are pushed. mark := s.mark() oneIteration := func(index, elem reflect.Value) { - // Set top var (lexically the second if there are two) to the element. if len(r.Pipe.Decl) > 0 { if r.Pipe.IsAssign { - s.setVar(r.Pipe.Decl[0].Ident[0], elem) + // With two variables, index comes first. + // With one, we use the element. + if len(r.Pipe.Decl) > 1 { + s.setVar(r.Pipe.Decl[0].Ident[0], index) + } else { + s.setVar(r.Pipe.Decl[0].Ident[0], elem) + } } else { + // Set top var (lexically the second if there + // are two) to the element. s.setTopVar(1, elem) } } - // Set next var (lexically the first if there are two) to the index. if len(r.Pipe.Decl) > 1 { if r.Pipe.IsAssign { - s.setVar(r.Pipe.Decl[1].Ident[0], index) + s.setVar(r.Pipe.Decl[1].Ident[0], elem) } else { + // Set next var (lexically the first if there + // are two) to the index. s.setTopVar(2, index) } } diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go index 45edb9e9bb2..b44d61bb116 100644 --- a/tpl/internal/go_templates/texttemplate/exec_test.go +++ b/tpl/internal/go_templates/texttemplate/exec_test.go @@ -697,6 +697,7 @@ var execTests = []execTest{ {"bug18c", "{{eq . 'P'}}", "true", 'P', true}, {"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true}, + {"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true}, } func zeroArgs() string { diff --git a/tpl/template.go b/tpl/template.go index 7a793101c51..91a482c2504 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -169,6 +169,13 @@ func SetPageInContext(ctx context.Context, p page) context.Context { return context.WithValue(ctx, texttemplate.PageContextKey, p) } +// SetSecurityAllowActionJSTmpl sets the global setting for allowing tempalte actions in JS template literals. +// This was added in Hugo 0.114.0. +// See https://github.com/golang/go/issues/59234 +func SetSecurityAllowActionJSTmpl(b bool) { + htmltemplate.SecurityAllowActionJSTmpl.Store(b) +} + type page interface { IsNode() bool } diff --git a/tpl/tplimpl/integration_test.go b/tpl/tplimpl/integration_test.go index 4107a1faa35..fa511fbab35 100644 --- a/tpl/tplimpl/integration_test.go +++ b/tpl/tplimpl/integration_test.go @@ -2,6 +2,7 @@ package tplimpl_test import ( "path/filepath" + "strings" "testing" qt "github.com/frankban/quicktest" @@ -160,3 +161,70 @@ title: "S3P1" b.AssertFileContent("public/s2/p1/index.html", `S2P1`) b.AssertFileContent("public/s3/p1/index.html", `S3P1`) } + +func TestGoTemplateBugs(t *testing.T) { + + t.Run("Issue 11112", func(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +-- layouts/index.html -- +{{ $m := dict "key" "value" }} +{{ $k := "" }} +{{ $v := "" }} +{{ range $k, $v = $m }} +{{ $k }} = {{ $v }} +{{ end }} + ` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ) + b.Build() + + b.AssertFileContent("public/index.html", `key = value`) + }) + +} + +func TestSecurityAllowActionJSTmpl(t *testing.T) { + + filesTemplate := ` +-- config.toml -- +SECURITYCONFIG +-- layouts/index.html -- + + ` + + files := strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", "") + + b, err := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).BuildE() + + b.Assert(err, qt.Not(qt.IsNil)) + b.Assert(err.Error(), qt.Contains, "{{.Title}} appears in a JS template literal") + + files = strings.ReplaceAll(filesTemplate, "SECURITYCONFIG", ` +[security] +[security.gotemplates] +allowActionJSTmpl = true +`) + + b = hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + +}