Cybersecurity News

Infinite Loop DoS in antchfx/xpath logicalQuery.Select()

Infinite Loop DoS in antchfx/xpath logicalQuery.Select()
antchfx/xpath infinite loop CPU DoS vulnerability
github.com/antchfx/xpath — XPath expression library for Go

Overview

github.com/antchfx/xpath is a pure-Go implementation of the XML Path Language (XPath) specification. It provides expression parsing, compilation, and evaluation against XML, HTML, and JSON document trees. The library is a foundational dependency for the antchfx family of document query packages and is used extensively across the Go ecosystem for structured document processing.

This report documents a denial-of-service vulnerability in antchfx/xpath v1.3.5, the current stable release. The vulnerability resides in logicalQuery.Select() inside query.go. When a boolean XPath expression that evaluates to true is used as a top-level node selector, the function enters an infinite loop, consuming 100% of a CPU core until the process is externally terminated. Expressions such as 0<1, 1=1, and true() all trigger this behaviour.

Background: XPath Node Selection in Go

The XPath specification defines two distinct uses of an expression. An expression can be evaluated for its value (a boolean, a number, a string, or a node-set), or it can be used as a location path to select a set of nodes from a document. In the latter case, the expression is compiled into a query object and iterated: the caller repeatedly invokes a Select() method until it returns nil, which signals that no further matching nodes remain.

The Go implementation of this iterator model depends on a fundamental contract: every Select() implementation must eventually return nil. Query types that can match at most one node track a count or done flag, increment it on the first successful return, and return nil on all subsequent calls. This ensures that every iteration loop terminates. The logicalQuery type, which handles relational and equality operators as well as boolean functions, is missing this mechanism entirely.

The Vulnerability: Missing Done-Sentinel in logicalQuery.Select()

The logicalQuery.Select() method in query.go is implemented as follows:

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
        }
    }
    return nil
}

When the expression evaluates to true, the function returns t.Current().Copy(). On every subsequent call it evaluates the expression again, obtains true again, and returns the same node again. The function carries no state. For a constant expression like 0<1, which is unconditionally and permanently true, Select() will return a non-nil value on every single invocation.

The caller of Select() is NodeIterator.MoveNext() in xpath.go:

func (t *NodeIterator) MoveNext() bool {
    n := t.query.Select(t)
    if n == nil {
        return false
    }
    if !t.node.MoveTo(n) {
        t.node = n.Copy()
    }
    return true
}

Since Select() never returns nil, MoveNext() always returns true. The iteration loop in QuerySelectorAll:

for t.MoveNext() {
    elems = append(elems, getCurrentNode(t))
}

runs without bound. The goroutine is permanently occupied, the function never returns, memory is allocated on every iteration, and one CPU core is pegged at 100% utilisation until the process is killed from outside.

Why Other Query Types Avoid This

The same file contains contextQuery, which correctly handles the single-result case using a counter:

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

After returning the current node once, c.count is 1 and every subsequent call returns nil immediately. The iteration terminates. absoluteQuery uses an identical pattern. logicalQuery was implemented without this guard, leaving it as the only query type in the file that can loop indefinitely.

Trigger Expressions

Any XPath expression that compiles to a top-level logicalQuery and produces true triggers the infinite loop:

ExpressionTypeBehaviour
0<1RelationalInfinite loop
1=1EqualityInfinite loop
1>0RelationalInfinite loop
0<=1RelationalInfinite loop
1!=2EqualityInfinite loop
true()Boolean functionInfinite loop
not(false())Boolean functionInfinite loop
0>1Relational (false)Returns normally
1=2Equality (false)Returns normally

Expressions that evaluate to false return normally because Select() returns nil on the first call. Only expressions that evaluate to true trigger the loop.

Threat Model and Attack Surface

The vulnerability is relevant to any application that accepts XPath expressions from user-controlled input and passes them to any of the affected query functions. This pattern appears in several common contexts:

  • XML and HTML scraping services that let users supply custom XPath selectors to target specific content in documents.
  • Document transformation pipelines where XPath expressions are stored in configuration files, database records, or user-defined processing rules.
  • API gateways and web services that accept XPath as a query language for structured data endpoints.
  • Testing and automation frameworks that expose XPath-based element selection to end users or external integrations.

The attack requires no authentication and no knowledge of the target document structure. The expression 1=1 is four bytes. It requires no special characters, no encoding tricks, and no understanding of the document being queried. Any input validation focused on document content or XPath syntax validity will pass this expression, as it is syntactically correct and semantically meaningful. Only a runtime evaluation guard would catch it.

In a web service context, a single HTTP request containing the expression true() as an XPath parameter is sufficient to permanently occupy the goroutine handling that request. With repeated requests, all available goroutines can be exhausted, taking the service offline entirely.

Severity

The vulnerability is assigned a CVSS 3.1 base score of 7.5 (High):

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
MetricValueRationale
Attack VectorNetworkXPath expression delivered over any network interface accepting user input
Attack ComplexityLowTrivial payload, no special encoding required
Privileges RequiredNoneNo authentication or account required
User InteractionNoneTriggered automatically on expression evaluation
ScopeUnchangedImpact confined to the target process
Confidentiality ImpactNoneNo data disclosed
Integrity ImpactNoneNo data modified
Availability ImpactHighPermanent goroutine stall and CPU exhaustion until process restart

CWE-835: Loop with Unreachable Exit Condition.

Remediation

The fix requires adding a done bool field to logicalQuery to track whether Select() has already returned a result. The field is set on the first successful return and reset when Evaluate() is called to re-initialise the query:

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

This brings logicalQuery into line with the termination contract that all other query types in the library already satisfy. Applications that cannot immediately update the library can apply a partial mitigation by validating XPath expressions against a known-safe allow-list before passing them to the query functions, though this is not a substitute for the upstream fix.

Disclosure

This vulnerability has been reported to the maintainer via a GitHub issue in the antchfx/xpath repository. No patch has been released at the time of publication. Users are advised to monitor the repository and update to a fixed version as soon as one becomes available.

Conclusion

The infinite loop in logicalQuery.Select() is a straightforward iterator contract violation. A Select() implementation that is required to eventually return nil never does when its underlying expression is permanently true. The absence of a done-sentinel, a pattern correctly implemented in every other query type in the same file, is the sole cause of the vulnerability.

The practical consequence for applications that pass user-controlled XPath expressions to the affected query functions is an easily triggered, zero-privilege, network-accessible denial-of-service. The trigger expression is syntactically valid, semantically meaningful, and indistinguishable from a legitimate query at the input validation layer. The only reliable defence is a patched version of the library.