diff --git a/binding/xml.go b/binding/xml.go
index acd6f942..c1a13c76 100644
--- a/binding/xml.go
+++ b/binding/xml.go
@@ -27,6 +27,7 @@ func (xmlBinding) BindBody(body []byte, obj any) error {
func decodeXML(r io.Reader, obj any) error {
decoder := xml.NewDecoder(r)
+ decoder.Entity = xml.HTMLEntity
if err := decoder.Decode(obj); err != nil {
return err
}
diff --git a/binding/xml_invariant_test.go b/binding/xml_invariant_test.go
new file mode 100644
index 00000000..f6651bc6
--- /dev/null
+++ b/binding/xml_invariant_test.go
@@ -0,0 +1,196 @@
+package binding
+
+import (
+ "bytes"
+ "runtime"
+ "testing"
+ "time"
+)
+
+// TestXMLBindingAdversarialInputs verifies that the XML binding maintains
+// security boundaries under adversarial inputs including XML bomb / billion
+// laughs attacks and other malicious XML payloads.
+//
+// Security invariant: parsing adversarial XML must not cause unbounded memory
+// growth or hang the process. The binding must either complete within a
+// reasonable time/memory budget or return an error — it must never silently
+// consume excessive resources.
+func TestXMLBindingAdversarialInputs(t *testing.T) {
+ payloads := []struct {
+ name string
+ payload string
+ }{
+ {
+ name: "billion_laughs_classic",
+ payload: `
+
+
+
+
+
+
+
+
+
+]>
+&lol9;`,
+ },
+ {
+ name: "billion_laughs_shallow",
+ payload: `
+
+
+
+
+]>
+&d;`,
+ },
+ {
+ name: "quadratic_blowup",
+ payload: `
+
+]>
+&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;`,
+ },
+ {
+ name: "deeply_nested_elements",
+ payload: func() string {
+ var buf bytes.Buffer
+ buf.WriteString(``)
+ depth := 100000
+ for i := 0; i < depth; i++ {
+ buf.WriteString("")
+ }
+ buf.WriteString("deep")
+ for i := 0; i < depth; i++ {
+ buf.WriteString("")
+ }
+ return buf.String()
+ }(),
+ },
+ {
+ name: "large_attribute_value",
+ payload: func() string {
+ var buf bytes.Buffer
+ buf.WriteString(`value`)
+ return buf.String()
+ }(),
+ },
+ {
+ name: "many_attributes",
+ payload: func() string {
+ var buf bytes.Buffer
+ buf.WriteString(`content`)
+ return buf.String()
+ }(),
+ },
+ {
+ name: "entity_in_attribute",
+ payload: `
+
+
+
+]>
+value`,
+ },
+ {
+ name: "malformed_xml",
+ payload: `text`,
+ },
+ {
+ name: "null_bytes",
+ payload: "\x00\x00\x00",
+ },
+ {
+ name: "unicode_bomb",
+ payload: `
+
+
+
+]>
+&u3;`,
+ },
+ }
+
+ // Memory limit: 256 MB growth allowed per parse attempt
+ const maxMemoryGrowthBytes = 256 * 1024 * 1024
+ // Time limit per parse attempt
+ const maxDuration = 5 * time.Second
+
+ type Target struct {
+ Value string `xml:",chardata"`
+ Attr string `xml:",attr"`
+ }
+
+ xmlBind := xmlBinding{}
+
+ for _, tc := range payloads {
+ t.Run(tc.name, func(t *testing.T) {
+ // Measure baseline memory
+ var memBefore runtime.MemStats
+ runtime.GC()
+ runtime.ReadMemStats(&memBefore)
+
+ done := make(chan error, 1)
+ start := time.Now()
+
+ go func() {
+ var obj Target
+ err := xmlBind.BindBody([]byte(tc.payload), &obj)
+ done <- err
+ }()
+
+ select {
+ case err := <-done:
+ elapsed := time.Since(start)
+
+ // Measure memory after
+ var memAfter runtime.MemStats
+ runtime.GC()
+ runtime.ReadMemStats(&memAfter)
+
+ // Security invariant 1: must complete within time limit
+ if elapsed > maxDuration {
+ t.Errorf("SECURITY VIOLATION: XML parsing took %v (limit %v) for payload %q — possible DoS",
+ elapsed, maxDuration, tc.name)
+ }
+
+ // Security invariant 2: memory growth must be bounded
+ if memAfter.TotalAlloc > memBefore.TotalAlloc {
+ growth := memAfter.TotalAlloc - memBefore.TotalAlloc
+ if growth > maxMemoryGrowthBytes {
+ t.Errorf("SECURITY VIOLATION: XML parsing allocated %d bytes (limit %d) for payload %q — possible memory bomb",
+ growth, maxMemoryGrowthBytes, tc.name)
+ }
+ }
+
+ // Security invariant 3: if parsing succeeded, the result must not
+ // be astronomically large (entity expansion must be bounded)
+ if err == nil {
+ // A successful parse of a bomb payload is a security concern
+ // if the result is huge; log it as a warning
+ t.Logf("payload %q parsed without error in %v (err=%v)", tc.name, elapsed, err)
+ }
+
+ case <-time.After(maxDuration):
+ t.Errorf("SECURITY VIOLATION: XML parsing timed out after %v for payload %q — DoS vulnerability confirmed",
+ maxDuration, tc.name)
+ }
+ })
+ }
+}