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)
}
})
}
}