Skip to content

Infinite loop in logicalQuery.Select() when boolean XPath expression evaluates to true #121

@athuljayaram

Description

@athuljayaram

Package: github.com/antchfx/xpath v1.3.5
File: query.gologicalQuery.Select()
CVSS 3.1: 7.5 High — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-835: Loop with Unreachable Exit Condition


Summary

Any boolean XPath expression that evaluates to true - such as 0<1,
1=1, 1>0, 0<=1, 1!=2, not(false()), true() - causes an
infinite loop when used as a top-level node selector, consuming 100% of
a CPU core until the process is killed.



Impact

Any application that accepts user-controlled input as an XPath expression
and passes it to Find(), QueryAll(), or QuerySelectorAll() in any
of the downstream antchfx/*query packages is vulnerable to an
unbounded CPU denial-of-service. A single request with a trivial
expression like 1=1 is sufficient to permanently consume a goroutine
and peg a CPU core until the process is restarted.


Root Cause

logicalQuery.Select() in query.go has no "done" sentinel. When the
boolean expression evaluates to true, it returns t.Current().Copy()
on every call and never returns nil:

go
func (l *logicalQuery) Select(t iterator) NodeNavigator {
node := t.Current().Copy()
val := l.Evaluate(t)
switch val.(type) {
case bool:
if val.(bool) == true {
return node // ← returns same node on every call, never nil
}
}
return nil
}

MoveNext() in NodeIterator loops until Select() returns nil:

func (t *NodeIterator) MoveNext() bool {
n := t.query.Select(t)
if n == nil { return false } // ← never reached
...
return true
}

Since expressions like 0<1 are always true, Select() never returns
nil, MoveNext() never returns false, and the calling loop in
QuerySelectorAll runs forever.


Compare to the Correct Pattern

contextQuery.Select() in the same file correctly tracks a count and
returns nil after the first result:

func (c *contextQuery) Select(t iterator) NodeNavigator {
if c.count > 0 {
return nil // terminates correctly ✓
}
c.count++
return t.Current().Copy()
}

logicalQuery is missing the equivalent guard entirely.


Affected Expressions

Any XPath expression that compiles to a top-level logicalQuery and
evaluates to true:

┌──────────────┬──────────────────────────┐
│ Expression │ Result │
├──────────────┼──────────────────────────┤
│ 0<1 │ ∞ loop │
├──────────────┼──────────────────────────┤
│ 1=1 │ ∞ loop │
├──────────────┼──────────────────────────┤
│ 1>0 │ ∞ loop │
├──────────────┼──────────────────────────┤
│ 0<=1 │ ∞ loop │
├──────────────┼──────────────────────────┤
│ 1!=2 │ ∞ loop │
├──────────────┼──────────────────────────┤
│ true() │ ∞ loop │
├──────────────┼──────────────────────────┤
│ not(false()) │ ∞ loop │
├──────────────┼──────────────────────────┤
│ 0>1 │ returns normally (false) │
├──────────────┼──────────────────────────┤
│ 1=2 │ returns normally (false) │
└──────────────┴──────────────────────────┘

Suggested Fix

Add a done bool field to logicalQuery, set it on first return, and
reset it in Evaluate:

type logicalQuery struct {
Left, Right query
Do func(iterator, interface{}, interface{}) interface{}
done bool
}

func (l *logicalQuery) Select(t iterator) NodeNavigator {
if l.done {
return nil
}
node := t.Current().Copy()
val := l.Evaluate(t)
if v, ok := val.(bool); ok && v {
l.done = true
return node
}
return nil
}

func (l *logicalQuery) Evaluate(t iterator) interface{} {
l.done = false
m := l.Left.Evaluate(t)
n := l.Right.Evaluate(t)
return l.Do(t, m, n)
}

func (l *logicalQuery) Clone() query {
return &logicalQuery{Left: l.Left.Clone(), Right: l.Right.Clone(), Do: l.Do}
}

Reference

https://securityinfinity.com/research/infinite-loop-dos-in-antchfx-xpath-logicalquery-select/

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions