VYPR
High severity7.5GHSA Advisory· Published May 19, 2026· Updated May 19, 2026

Dasel: Denial of service in dasel selector lexer due to infinite loop on unterminated regex literal

CVE-2026-46378

Description

Summary

dasel's selector lexer enters a non-terminating loop when tokenizing an unterminated regex pattern such as r/abc. A 2-byte input (r/) is sufficient to cause the tokenizer to consume 100% CPU on one core indefinitely.

I confirmed the issue on v3.3.1 (fba653c7f248aff10f2b89fca93929b64707dfc8) and on master commit 0dd6132e0c58edbd9b1a5f7ffd00dfab1e6085ad. I also verified the same code path is present in v3.0.0 (648f83baf070d9e00db8ff312febef857ec090a3). No fix is available yet.

Details

The bug is in the matchRegexPattern closure within (*Tokenizer).parseCurRune in `selector/lexer/tokenize.go#L237-L247`:

matchRegexPattern := func(pos int) *Token {
    if p.src[pos] != 'r' || !p.peekRuneEqual(pos+1, '/') {
        return nil
    }
    start := pos
    pos += 2
    for !p.peekRuneEqual(pos, '/') {  // line 243
        pos++
    }
    pos++
    return ptr.To(NewToken(RegexPattern, p.src[start+2:pos-1], start, pos-start))
}

When no closing / exists, `peekRuneEqual` returns false when pos >= srcLen (because the bounds check at line 40 returns false for out-of-range positions). Since !false = true, the loop condition remains true and pos increments indefinitely. The function never returns.

Notably, the same function already handles unterminated quoted strings by returning UnexpectedEOFError, but the regex pattern path does not perform a similar end-of-input check.

Minimal trigger: r/ (2 bytes)

Test environment:

  • MacBook Air (Apple M2), macOS / Darwin arm64
  • Go 1.26.1
  • dasel v3.3.1 (fba653c7f248aff10f2b89fca93929b64707dfc8)

PoC

package main

import (
	"fmt"
	"runtime"
	"time"

	"github.com/tomwright/dasel/v3/selector/lexer"
)

func main() {
	fmt.Printf("Go version: %s\n", runtime.Version())
	fmt.Printf("GOARCH: %s\n", runtime.GOARCH)
	fmt.Println()

	for _, input := range []string{"r/unterminated", "r/"} {
		fmt.Printf("Input: %s\n", input)
		done := make(chan string, 1)
		go func() {
			t := lexer.NewTokenizer(input)
			start := time.Now()
			tokens, err := t.Tokenize()
			elapsed := time.Since(start)
			if err != nil {
				done <- fmt.Sprintf("Error after %v: %v", elapsed, err)
			} else {
				done <- fmt.Sprintf("OK after %v: %d tokens", elapsed, len(tokens))
			}
		}()

		select {
		case result := <-done:
			fmt.Println(result)
		case <-time.After(5 * time.Second):
			fmt.Println("CONFIRMED: did not complete within 5s; tokenizer is stuck in non-terminating loop")
		}
		fmt.Println()
	}
}

Observed output on v3.3.1 in the test environment above:

Go version: go1.26.1
GOARCH: arm64

Input: r/unterminated
CONFIRMED: did not complete within 5s; tokenizer is stuck in non-terminating loop

Input: r/
CONFIRMED: did not complete within 5s; tokenizer is stuck in non-terminating loop

Impact

An attacker who can control or influence the selector/query string passed to dasel can cause the tokenizer to enter a non-terminating loop. The affected process consumes 100% CPU on one core and does not make progress until externally terminated.

The selector string is typically provided by the application developer, but there are deployment scenarios where it may be attacker-influenced: - Web applications using dasel for dynamic data querying - Applications that construct selectors from user input - Shared tooling environments where selectors are passed as parameters

Suggested

Fix

The regex scanner should bounds-check and return an error on unterminated regex literals, consistent with unterminated quoted strings. Since matchRegexPattern currently returns *Token, the fix also requires changing the function signature to propagate errors. For example:

matchRegexPattern := func(pos int) (*Token, error) {
    if p.src[pos] != 'r' || !p.peekRuneEqual(pos+1, '/') {
        return nil, nil
    }
    start := pos
    pos += 2
    for pos < p.srcLen && p.src[pos] != '/' {
        pos++
    }
    if pos >= p.srcLen {
        return nil, &UnexpectedEOFError{Pos: pos}
    }
    pos++
    return ptr.To(NewToken(RegexPattern, p.src[start+2:pos-1], start, pos-start)), nil
}

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A missing bounds check in dasel's selector lexer causes an infinite loop consuming 100% CPU on a single core when tokenizing an unterminated regex like 'r/'.

Vulnerability

The dasel selector lexer contains a missing end-of-input check in the matchRegexPattern closure within (*Tokenizer).parseCurRune in selector/lexer/tokenize.go (lines 237–247) [1][2]. When an unterminated regex literal (e.g. r/abc or just r/) is provided, the for !p.peekRuneEqual(pos, '/') loop never finds the closing / because peekRuneEqual returns false once pos exceeds the source length, and !false keeps the loop running indefinitely. The issue affects versions v3.0.0, v3.3.1, and master commit 0dd6132e0c58edbd9b1a5f7ffd00dfab1e6085ad [1][2]. No fix is available in the latest release as of the advisory publication date [1][2].

Exploitation

An attacker can trigger the infinite loop by providing a selector string containing a regex literal that lacks a closing /. The minimal input r/ (2 bytes) is sufficient [1][2]. No authentication, special privileges, or user interaction beyond submitting the crafted selector to dasel is required. Any component that processes user-supplied selectors — including dasel CLI invocations or Go libraries that call lexer.Tokenize — is vulnerable to a denial-of-service condition.

Impact

Successful exploitation causes the tokenizer to enter an infinite loop, consuming 100% CPU on one core indefinitely [1][2]. This results in a denial of service for the affected process. No confidentiality or integrity impact occurs; the attack is purely resource-exhaustion based.

Mitigation

As of the advisory publication date (2026-05-19), no patched release of dasel is available [1][2]. A commit (95f8dd3af12958bf6ca2a737b3ec0267280f86ed) has been proposed on GitHub that adds a bounds check and returns an UnexpectedEOFError when the position exceeds the source length [4]. Users should monitor the dasel repository for a new release containing this fix. Until then, avoid processing untrusted selector input, or apply the commit manually if building from source.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Tomwright/DaselGHSA2 versions
    >= 3.0.0, < 3.10.1+ 1 more
    • (no CPE)range: >= 3.0.0, < 3.10.1
    • (no CPE)range: <=3.3.1

Patches

1
95f8dd3af129

Merge commit from fork

https://github.com/TomWright/daselTom WrightMay 13, 2026via ghsa
3 files changed · +20 5
  • CHANGELOG.md+1 0 modified
    @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
     
     ### Fixed
     
    +- Fixed a non-terminating loop in the selector lexer when tokenizing unterminated regex patterns (e.g. `r/abc`). The tokenizer now returns an error instead of looping indefinitely.
     - Fix panic when selector query contains a trailing backslash in a quoted string ([GHSA-m5j3-4634-c2vq](https://github.com/TomWright/dasel/security/advisories/GHSA-m5j3-4634-c2vq)).
     
     ## [v3.10.0] - 2026-05-13
    
  • selector/lexer/tokenize.go+11 5 modified
    @@ -271,17 +271,20 @@ func (p *Tokenizer) parseCurRune() (Token, error) {
     			return ptr.To(NewToken(kind, other, pos, l))
     		}
     
    -		matchRegexPattern := func(pos int) *Token {
    +		matchRegexPattern := func(pos int) (*Token, error) {
     			if p.src[pos] != 'r' || !p.peekRuneEqual(pos+1, '/') {
    -				return nil
    +				return nil, nil
     			}
     			start := pos
     			pos += 2
    -			for !p.peekRuneEqual(pos, '/') {
    +			for pos < p.srcLen && p.src[pos] != '/' {
     				pos++
     			}
    +			if pos >= p.srcLen {
    +				return nil, &UnexpectedEOFError{Pos: pos}
    +			}
     			pos++
    -			return ptr.To(NewToken(RegexPattern, p.src[start+2:pos-1], start, pos-start))
    +			return ptr.To(NewToken(RegexPattern, p.src[start+2:pos-1], start, pos-start)), nil
     		}
     
     		if t := matchStr(pos, "null", true, Null); t != nil {
    @@ -345,7 +348,10 @@ func (p *Tokenizer) parseCurRune() (Token, error) {
     			return *t, nil
     		}
     
    -		if t := matchRegexPattern(pos); t != nil {
    +		if t, err := matchRegexPattern(pos); t != nil || err != nil {
    +			if err != nil {
    +				return Token{}, err
    +			}
     			return *t, nil
     		}
     
    
  • selector/lexer/tokenize_test.go+8 0 modified
    @@ -181,5 +181,13 @@ func TestTokenizer_Parse(t *testing.T) {
     			in:    `'\`,
     			match: matchUnexpectedEOFError(2),
     		}.run)
    +		t.Run("unterminated regex", errTestCase{
    +			in:    `r/unterminated`,
    +			match: matchUnexpectedEOFError(14),
    +		}.run)
    +		t.Run("unterminated regex minimal", errTestCase{
    +			in:    `r/`,
    +			match: matchUnexpectedEOFError(2),
    +		}.run)
     	})
     }
    

Vulnerability mechanics

Root cause

"Missing bounds check in the regex pattern scanner loop causes infinite increment of the position index when no closing slash exists."

Attack vector

An attacker provides a selector string containing an unterminated regex literal such as `r/` or `r/abc` to the dasel tokenizer. The `matchRegexPattern` closure in `parseCurRune` [patch_id=646460] enters a non-terminating loop because the `for` condition `!p.peekRuneEqual(pos, '/')` never becomes false when the input ends before a closing `/`. The loop increments `pos` indefinitely, consuming 100% CPU on one core. No authentication or special privileges are required; the attacker only needs control over the selector string passed to dasel.

Affected code

The bug is in the `matchRegexPattern` closure within `(*Tokenizer).parseCurRune` in `selector/lexer/tokenize.go` [patch_id=646460]. The loop at the original line 243 (`for !p.peekRuneEqual(pos, '/')`) lacks a bounds check against `p.srcLen`, causing infinite iteration when no closing `/` exists. The `peekRuneEqual` helper returns `false` for out-of-range positions, so the loop condition never becomes false.

What the fix does

The patch [patch_id=646460] replaces the unbounded `for !p.peekRuneEqual(pos, '/')` loop with a bounds-checked `for pos < p.srcLen && p.src[pos] != '/'` loop. After the loop exits, a check `if pos >= p.srcLen` returns `UnexpectedEOFError`, consistent with how unterminated quoted strings are already handled. The function signature also changes from returning `*Token` to `(*Token, error)` so the error can propagate up through the caller. This closes the infinite loop by ensuring the scanner stops and reports an error when the input is exhausted.

Preconditions

  • inputAttacker must be able to supply a selector string containing an unterminated regex literal (e.g. `r/` or `r/abc`) to the dasel tokenizer.

Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.