From 76631d0e12dde9aa96f5bd1facb9212d3c45a6ab Mon Sep 17 00:00:00 2001 From: mohammadmseet-hue Date: Tue, 7 Apr 2026 18:31:31 +0200 Subject: [PATCH] encoding/xml: validate element and attribute names in Encoder The xml.Encoder and xml.Marshal functions write element and attribute names directly to the output without validation. If an application passes attacker-controlled strings as Name.Local (via XMLName fields, EncodeElement, or EncodeToken), the output contains unescaped XML special characters, enabling XML injection. This is inconsistent with the existing validation for ProcInst targets (which calls isNameString) and the escaping of namespace values (which calls EscapeString). The fix adds isNameString validation to writeStart (for both element names and attribute names) and writeEnd, matching the validation already applied to ProcInst targets. Invalid names now return an error instead of producing malformed XML. Added TestEncodeTokenInvalidNames and TestMarshalInvalidXMLName covering element name injection, attribute name injection, and ProcInst validation as control. --- src/encoding/xml/marshal.go | 15 ++++++- src/encoding/xml/marshal_test.go | 74 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/encoding/xml/marshal.go b/src/encoding/xml/marshal.go index 13fbeeeedc75ce..f2da0e006a01d7 100644 --- a/src/encoding/xml/marshal.go +++ b/src/encoding/xml/marshal.go @@ -733,6 +733,9 @@ func (p *printer) writeStart(start *StartElement) error { if start.Name.Local == "" { return fmt.Errorf("xml: start tag with no name") } + if !isNameString(start.Name.Local) { + return fmt.Errorf("xml: start tag with invalid name: %s", start.Name.Local) + } p.tags = append(p.tags, start.Name) p.markPrefix() @@ -753,9 +756,16 @@ func (p *printer) writeStart(start *StartElement) error { if name.Local == "" { continue } + if !isNameString(name.Local) { + return fmt.Errorf("xml: attribute with invalid name: %s", name.Local) + } p.WriteByte(' ') if name.Space != "" { - p.WriteString(p.createAttrPrefix(name.Space)) + prefix := p.createAttrPrefix(name.Space) + if !isNameString(prefix) { + return fmt.Errorf("xml: attribute prefix with invalid name: %s", prefix) + } + p.WriteString(prefix) p.WriteByte(':') } p.WriteString(name.Local) @@ -771,6 +781,9 @@ func (p *printer) writeEnd(name Name) error { if name.Local == "" { return fmt.Errorf("xml: end tag with no name") } + if !isNameString(name.Local) { + return fmt.Errorf("xml: end tag with invalid name: %s", name.Local) + } if len(p.tags) == 0 || p.tags[len(p.tags)-1].Local == "" { return fmt.Errorf("xml: end tag without start tag", name.Local) } diff --git a/src/encoding/xml/marshal_test.go b/src/encoding/xml/marshal_test.go index 6c7e711aac0566..b3df2a68ec1612 100644 --- a/src/encoding/xml/marshal_test.go +++ b/src/encoding/xml/marshal_test.go @@ -2588,3 +2588,77 @@ func TestClose(t *testing.T) { }) } } + +func TestEncodeTokenInvalidNames(t *testing.T) { + tests := []struct { + name string + token Token + }{ + { + name: "start element with angle bracket in name", + token: StartElement{Name: Name{Local: `div>bar"}, Value: "data"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Marshal(tt.input) + if err == nil { + t.Errorf("Marshal with invalid XMLName.Local=%q succeeded, want error", tt.input.XMLName.Local) + } + }) + } +}