Skip to content

Commit 0f1a332

Browse files
committed
Merge branch 'release-branch.go1.26' of https://go.googlesource.com/go into bradfitz/1_26_bump
Go 1.26.1
2 parents de220ef + e87b10e commit 0f1a332

16 files changed

Lines changed: 209 additions & 49 deletions

File tree

VERSION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
go1.26.0
2-
time 2026-02-10T01:22:00Z
1+
go1.26.1
2+
time 2026-03-05T20:45:11Z

doc/godebug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ and the [go command documentation](/cmd/go#hdr-Build_and_test_caching).
155155

156156
### Go 1.26
157157

158+
Go 1.26.1 added a new `htmlmetacontenturlescape` setting that controls whether
159+
html/template will escape URLs in the `url=` portion of the content attribute of
160+
HTML meta tags. The default `htmlmetacontentescape=1` will cause URLs to be
161+
escaped. Setting `htmlmetacontentescape=0` disables this behavior.
162+
158163
Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number
159164
of cookies that net/http will accept when parsing HTTP headers. If the number of
160165
cookie in a header exceeds the number set in `httpcookiemaxnum`, cookie parsing

src/crypto/x509/constraints.go

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ import (
5858
// of nameConstraintsSet, to handle constraints which define full email
5959
// addresses (i.e. 'test@example.com'). For bare domain constraints, we use the
6060
// dnsConstraints type described above, querying the domain portion of the email
61-
// address. For full email addresses, we also hold a map of email addresses that
62-
// map the local portion of the email to the domain. When querying full email
63-
// addresses we then check if the local portion of the email is present in the
64-
// map, and if so case insensitively compare the domain portion of the
65-
// email.
61+
// address. For full email addresses, we also hold a map of email addresses with
62+
// the domain portion of the email lowercased, since it is case insensitive. When
63+
// looking up an email address in the constraint set, we first check the full
64+
// email address map, and if we don't find anything, we check the domain portion
65+
// of the email address against the dnsConstraints.
6666

6767
type nameConstraintsSet[T *net.IPNet | string, V net.IP | string] struct {
6868
set []T
@@ -375,7 +375,7 @@ func (dnc *dnsConstraints) query(s string) (string, bool) {
375375
return constraint, true
376376
}
377377

378-
if !dnc.permitted && s[0] == '*' {
378+
if !dnc.permitted && len(s) > 0 && s[0] == '*' {
379379
trimmed := trimFirstLabel(s)
380380
if constraint, found := dnc.parentConstraints[trimmed]; found {
381381
return constraint, true
@@ -387,16 +387,22 @@ func (dnc *dnsConstraints) query(s string) (string, bool) {
387387
type emailConstraints struct {
388388
dnsConstraints interface{ query(string) (string, bool) }
389389

390-
fullEmails map[string]string
390+
// fullEmails is map of rfc2821Mailboxs that are fully specified in the
391+
// constraints, which we need to check for separately since they don't
392+
// follow the same matching rules as the domain-based constraints. The
393+
// domain portion of the rfc2821Mailbox has been lowercased, since the
394+
// domain portion is case insensitive. When checking the map for an email,
395+
// the domain portion of the query should also be lowercased.
396+
fullEmails map[rfc2821Mailbox]struct{}
391397
}
392398

393399
func newEmailConstraints(l []string, permitted bool) interface {
394-
query(parsedEmail) (string, bool)
400+
query(rfc2821Mailbox) (string, bool)
395401
} {
396402
if len(l) == 0 {
397403
return nil
398404
}
399-
exactMap := map[string]string{}
405+
exactMap := map[rfc2821Mailbox]struct{}{}
400406
var domains []string
401407
for _, c := range l {
402408
if !strings.ContainsRune(c, '@') {
@@ -411,7 +417,8 @@ func newEmailConstraints(l []string, permitted bool) interface {
411417
// certificate since parsing.
412418
continue
413419
}
414-
exactMap[parsed.local] = parsed.domain
420+
parsed.domain = strings.ToLower(parsed.domain)
421+
exactMap[parsed] = struct{}{}
415422
}
416423
ec := &emailConstraints{
417424
fullEmails: exactMap,
@@ -422,16 +429,16 @@ func newEmailConstraints(l []string, permitted bool) interface {
422429
return ec
423430
}
424431

425-
func (ec *emailConstraints) query(s parsedEmail) (string, bool) {
426-
if len(ec.fullEmails) > 0 && strings.ContainsRune(s.email, '@') {
427-
if domain, ok := ec.fullEmails[s.mailbox.local]; ok && strings.EqualFold(domain, s.mailbox.domain) {
428-
return ec.fullEmails[s.email] + "@" + s.mailbox.domain, true
432+
func (ec *emailConstraints) query(s rfc2821Mailbox) (string, bool) {
433+
if len(ec.fullEmails) > 0 {
434+
if _, ok := ec.fullEmails[s]; ok {
435+
return fmt.Sprintf("%s@%s", s.local, s.domain), true
429436
}
430437
}
431438
if ec.dnsConstraints == nil {
432439
return "", false
433440
}
434-
constraint, found := ec.dnsConstraints.query(s.mailbox.domain)
441+
constraint, found := ec.dnsConstraints.query(s.domain)
435442
return constraint, found
436443
}
437444

@@ -441,7 +448,7 @@ type constraints[T any, V any] struct {
441448
excluded interface{ query(V) (T, bool) }
442449
}
443450

444-
func checkConstraints[T string | *net.IPNet, V any, P string | net.IP | parsedURI | parsedEmail](c constraints[T, V], s V, p P) error {
451+
func checkConstraints[T string | *net.IPNet, V any, P string | net.IP | parsedURI | rfc2821Mailbox](c constraints[T, V], s V, p P) error {
445452
if c.permitted != nil {
446453
if _, found := c.permitted.query(s); !found {
447454
return fmt.Errorf("%s %q is not permitted by any constraint", c.constraintType, p)
@@ -459,13 +466,13 @@ type chainConstraints struct {
459466
ip constraints[*net.IPNet, net.IP]
460467
dns constraints[string, string]
461468
uri constraints[string, string]
462-
email constraints[string, parsedEmail]
469+
email constraints[string, rfc2821Mailbox]
463470

464471
index int
465472
next *chainConstraints
466473
}
467474

468-
func (cc *chainConstraints) check(dns []string, uris []parsedURI, emails []parsedEmail, ips []net.IP) error {
475+
func (cc *chainConstraints) check(dns []string, uris []parsedURI, emails []rfc2821Mailbox, ips []net.IP) error {
469476
for _, ip := range ips {
470477
if err := checkConstraints(cc.ip, ip, ip); err != nil {
471478
return err
@@ -488,8 +495,8 @@ func (cc *chainConstraints) check(dns []string, uris []parsedURI, emails []parse
488495
}
489496
}
490497
for _, e := range emails {
491-
if !domainNameValid(e.mailbox.domain, false) {
492-
return fmt.Errorf("x509: cannot parse rfc822Name %q", e.mailbox)
498+
if !domainNameValid(e.domain, false) {
499+
return fmt.Errorf("x509: cannot parse rfc822Name %q", e)
493500
}
494501
if err := checkConstraints(cc.email, e, e); err != nil {
495502
return err
@@ -509,7 +516,7 @@ func checkChainConstraints(chain []*Certificate) error {
509516
ip: constraints[*net.IPNet, net.IP]{"IP address", newIPNetConstraints(c.PermittedIPRanges), newIPNetConstraints(c.ExcludedIPRanges)},
510517
dns: constraints[string, string]{"DNS name", newDNSConstraints(c.PermittedDNSDomains, true), newDNSConstraints(c.ExcludedDNSDomains, false)},
511518
uri: constraints[string, string]{"URI", newDNSConstraints(c.PermittedURIDomains, true), newDNSConstraints(c.ExcludedURIDomains, false)},
512-
email: constraints[string, parsedEmail]{"email address", newEmailConstraints(c.PermittedEmailAddresses, true), newEmailConstraints(c.ExcludedEmailAddresses, false)},
519+
email: constraints[string, rfc2821Mailbox]{"email address", newEmailConstraints(c.PermittedEmailAddresses, true), newEmailConstraints(c.ExcludedEmailAddresses, false)},
513520
index: i,
514521
}
515522
if currentConstraints == nil {
@@ -592,24 +599,15 @@ func parseURIs(uris []*url.URL) ([]parsedURI, error) {
592599
return parsed, nil
593600
}
594601

595-
type parsedEmail struct {
596-
email string
597-
mailbox *rfc2821Mailbox
598-
}
599-
600-
func (e parsedEmail) String() string {
601-
return e.mailbox.local + "@" + e.mailbox.domain
602-
}
603-
604-
func parseMailboxes(emails []string) ([]parsedEmail, error) {
605-
parsed := make([]parsedEmail, 0, len(emails))
602+
func parseMailboxes(emails []string) ([]rfc2821Mailbox, error) {
603+
parsed := make([]rfc2821Mailbox, 0, len(emails))
606604
for _, email := range emails {
607605
mailbox, ok := parseRFC2821Mailbox(email)
608606
if !ok {
609607
return nil, fmt.Errorf("cannot parse rfc822Name %q", email)
610608
}
611609
mailbox.domain = strings.ToLower(mailbox.domain)
612-
parsed = append(parsed, parsedEmail{strings.ToLower(email), &mailbox})
610+
parsed = append(parsed, mailbox)
613611
}
614612
return parsed, nil
615613
}

src/crypto/x509/name_constraints_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,50 @@ var nameConstraintsTests = []nameConstraintsTest{
16121612
sans: []string{"dns:testexample.com"},
16131613
},
16141614
},
1615+
{
1616+
name: "excluded email constraint, multiple email with matching local portion",
1617+
roots: []constraintsSpec{
1618+
{
1619+
bad: []string{"email:a@example.com", "email:a@test.com"},
1620+
},
1621+
},
1622+
intermediates: [][]constraintsSpec{
1623+
{
1624+
{},
1625+
},
1626+
},
1627+
leaf: leafSpec{
1628+
sans: []string{"email:a@example.com"},
1629+
},
1630+
expectedError: "\"a@example.com\" is excluded by constraint \"a@example.com\"",
1631+
},
1632+
{
1633+
name: "email_case_check",
1634+
roots: []constraintsSpec{
1635+
{
1636+
ok: []string{"email:a@example.com"},
1637+
},
1638+
},
1639+
intermediates: [][]constraintsSpec{
1640+
{
1641+
{},
1642+
},
1643+
},
1644+
leaf: leafSpec{
1645+
sans: []string{"email:a@ExAmple.com"},
1646+
},
1647+
},
1648+
{
1649+
name: "excluded constraint, empty DNS san",
1650+
roots: []constraintsSpec{
1651+
{
1652+
bad: []string{"dns:example.com"},
1653+
},
1654+
},
1655+
leaf: leafSpec{
1656+
sans: []string{"dns:"},
1657+
},
1658+
},
16151659
}
16161660

16171661
func makeConstraintsCACert(constraints constraintsSpec, name string, key *ecdsa.PrivateKey, parent *Certificate, parentKey *ecdsa.PrivateKey) (*Certificate, error) {

src/crypto/x509/verify.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ type rfc2821Mailbox struct {
253253
local, domain string
254254
}
255255

256+
func (s rfc2821Mailbox) String() string {
257+
return fmt.Sprintf("%s@%s", s.local, s.domain)
258+
}
259+
256260
// parseRFC2821Mailbox parses an email address into local and domain parts,
257261
// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,
258262
// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The

src/html/template/attr_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/html/template/context.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ const (
156156
// stateError is an infectious error state outside any valid
157157
// HTML/CSS/JS construct.
158158
stateError
159+
// stateMetaContent occurs inside a HTML meta element content attribute.
160+
stateMetaContent
161+
// stateMetaContentURL occurs inside a "url=" tag in a HTML meta element content attribute.
162+
stateMetaContentURL
159163
// stateDead marks unreachable code after a {{break}} or {{continue}}.
160164
stateDead
161165
)
@@ -267,6 +271,8 @@ const (
267271
elementTextarea
268272
// elementTitle corresponds to the RCDATA <title> element.
269273
elementTitle
274+
// elementMeta corresponds to the HTML <meta> element.
275+
elementMeta
270276
)
271277

272278
//go:generate stringer -type attr
@@ -288,4 +294,6 @@ const (
288294
attrURL
289295
// attrSrcset corresponds to a srcset attribute.
290296
attrSrcset
297+
// attrMetaContent corresponds to the content attribute in meta HTML element.
298+
attrMetaContent
291299
)

src/html/template/element_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/html/template/escape.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ func (e *escaper) escape(c context, n parse.Node) context {
166166

167167
var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
168168

169+
var htmlmetacontenturlescape = godebug.New("htmlmetacontenturlescape")
170+
169171
// escapeAction escapes an action template node.
170172
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
171173
if len(n.Pipe.Decl) != 0 {
@@ -223,6 +225,18 @@ func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
223225
default:
224226
panic(c.urlPart.String())
225227
}
228+
case stateMetaContent:
229+
// Handled below in delim check.
230+
case stateMetaContentURL:
231+
if htmlmetacontenturlescape.Value() != "0" {
232+
s = append(s, "_html_template_urlfilter")
233+
} else {
234+
// We don't have a great place to increment this, since it's hard to
235+
// know if we actually escape any urls in _html_template_urlfilter,
236+
// since it has no information about what context it is being
237+
// executed in etc. This is probably the best we can do.
238+
htmlmetacontenturlescape.IncNonDefault()
239+
}
226240
case stateJS:
227241
s = append(s, "_html_template_jsvalescaper")
228242
// A slash after a value starts a div operator.

src/html/template/escape_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,16 @@ func TestEscape(t *testing.T) {
734734
"<script>var a = `${ var a = \"{{\"a \\\" d\"}}\" }`</script>",
735735
"<script>var a = `${ var a = \"a \\u0022 d\" }`</script>",
736736
},
737+
{
738+
"meta content attribute url",
739+
`<meta http-equiv="refresh" content="asd; url={{"javascript:alert(1)"}}; asd; url={{"vbscript:alert(1)"}}; asd">`,
740+
`<meta http-equiv="refresh" content="asd; url=#ZgotmplZ; asd; url=#ZgotmplZ; asd">`,
741+
},
742+
{
743+
"meta content string",
744+
`<meta http-equiv="refresh" content="{{"asd: 123"}}">`,
745+
`<meta http-equiv="refresh" content="asd: 123">`,
746+
},
737747
}
738748

739749
for _, test := range tests {
@@ -1016,6 +1026,14 @@ func TestErrors(t *testing.T) {
10161026
"<script>var tmpl = `asd ${return \"{\"}`;</script>",
10171027
``,
10181028
},
1029+
{
1030+
`{{if eq "" ""}}<meta>{{end}}`,
1031+
``,
1032+
},
1033+
{
1034+
`{{if eq "" ""}}<meta content="url={{"asd"}}">{{end}}`,
1035+
``,
1036+
},
10191037

10201038
// Error cases.
10211039
{
@@ -2198,3 +2216,19 @@ func TestAliasedParseTreeDoesNotOverescape(t *testing.T) {
21982216
t.Fatalf(`Template "foo" and "bar" rendered %q and %q respectively, expected equal values`, got1, got2)
21992217
}
22002218
}
2219+
2220+
func TestMetaContentEscapeGODEBUG(t *testing.T) {
2221+
savedGODEBUG := os.Getenv("GODEBUG")
2222+
os.Setenv("GODEBUG", savedGODEBUG+",htmlmetacontenturlescape=0")
2223+
defer func() { os.Setenv("GODEBUG", savedGODEBUG) }()
2224+
2225+
tmpl := Must(New("").Parse(`<meta http-equiv="refresh" content="asd; url={{"javascript:alert(1)"}}; asd; url={{"vbscript:alert(1)"}}; asd">`))
2226+
var b strings.Builder
2227+
if err := tmpl.Execute(&b, nil); err != nil {
2228+
t.Fatalf("unexpected error: %s", err)
2229+
}
2230+
want := `<meta http-equiv="refresh" content="asd; url=javascript:alert(1); asd; url=vbscript:alert(1); asd">`
2231+
if got := b.String(); got != want {
2232+
t.Fatalf("got %q, want %q", got, want)
2233+
}
2234+
}

0 commit comments

Comments
 (0)