Skip to main content
Vulnerability Database/CVE-2026-67987

CVE-2026-67987: Ruby LLM Regex DoS Vulnerability

CVE-2026-67987 is a polynomial-time regex denial-of-service flaw in crmne/ruby_llm affecting Ruby 3.1.x that allows excessive CPU consumption through malicious model responses. This post explains its impact, affected versions, and mitigation steps.

Published:

CVE-2026-67987 Overview

CVE-2026-67987 is a polynomial-time regular expression denial-of-service (ReDoS) vulnerability in the crmne/ruby_llm Ruby library. The flaw affects commit fa6f279847d6d7027814539d9c0dfc3bbdfd2a83 when running on Ruby 3.1.x. A crafted or anomalous model response containing many unterminated <think> tags forces two consecutive regular expressions in the chat-completion parser to consume excessive CPU. The result delays chat-completion processing and can starve the host process of resources.

Critical Impact

A malicious or malformed LLM response can trigger CPU exhaustion in ruby_llm chat-completion parsing, degrading availability of any Ruby service that relays untrusted model output.

Affected Products

  • crmne/ruby_llm at commit fa6f279847d6d7027814539d9c0dfc3bbdfd2a83
  • Ruby runtime version 3.1.x
  • Downstream Ruby applications performing chat-completion parsing through ruby_llm

Discovery Timeline

  • 2026-09-29 - CVE-2026-67987 published to NVD
  • 2026-09-29 - Last updated in NVD database

Technical Details for CVE-2026-67987

Vulnerability Analysis

The ruby_llm library parses assistant responses to separate visible content from reasoning traces wrapped in <think>...</think> tags. The parsing logic in lib/ruby_llm/protocols/chat_completions/chat.rb at lines 355–356 applies two consecutive regular expressions to the raw response string. When the response contains many unterminated <think> tags, the regex engine exhibits polynomial time complexity while attempting to match nested and repeated patterns.

The vulnerability is an algorithmic complexity issue rather than a memory-safety flaw. Any caller that forwards untrusted model output through extract_content_and_thinking can trigger the condition. Because Ruby processes are typically single-threaded per request, sustained CPU exhaustion blocks legitimate chat-completion traffic and can cascade into broader service degradation.

Root Cause

The root cause is unbounded backtracking in the two consecutive regular expressions used to extract <think> tag content from string responses. The parser assumed well-formed model output and did not cap iteration or reject malformed sequences of opening tags without closures. Streaming accumulation logic in lib/ruby_llm/stream_accumulator.rb compounded the issue by tracking @inside_think_tag and @pending_think_tag state that fed additional partial data into the same regex pipeline.

Attack Vector

An attacker who controls or influences model responses (for example, through prompt injection, a malicious model server, or a compromised upstream provider) can return a payload with numerous unterminated <think> tags. The Ruby host consuming that response invokes the vulnerable parser and burns CPU cycles proportional to the number of malformed tags. Repeated requests amplify the effect into a denial-of-service condition.

ruby
# Security patch in lib/ruby_llm/protocols/chat_completions/chat.rb
# Stop parsing <think> tags out of assistant content
        end

        def extract_content_and_thinking(content)
-          return extract_think_tag_content(content) if content.is_a?(String)
          return [content, nil] unless content.is_a?(Array)

          text = extract_text_from_blocks(content)
# Source: https://github.com/crmne/ruby_llm/commit/5e88411f171721b381853fa77d254e266dcf6ad8
ruby
# Security patch in lib/ruby_llm/stream_accumulator.rb
# Removes think-tag state tracking from the streaming accumulator
      @raw_content = nil
      @raw_reasoning = nil
      @finish_reason = nil
-      @inside_think_tag = false
-      @pending_think_tag = +''
      @latest_tool_call_id = nil
      @tool_call_ids_by_index = {}
    end
# Source: https://github.com/crmne/ruby_llm/commit/5e88411f171721b381853fa77d254e266dcf6ad8

Detection Methods for CVE-2026-67987

Indicators of Compromise

  • Sustained single-core CPU saturation in Ruby worker processes correlated with inbound LLM responses.
  • Chat-completion request latency spikes without matching increases in downstream model provider latency.
  • Assistant response payloads containing repeated <think> substrings with no matching </think> closures.

Detection Strategies

  • Log the raw byte length and <think> tag count of each model response before invoking extract_content_and_thinking, and alert on outliers.
  • Add per-request wall-clock timers around chat-completion parsing and flag calls that exceed a millisecond threshold.
  • Inspect application performance monitoring traces for stack frames stuck inside regex evaluation from chat.rb:355-356.

Monitoring Recommendations

  • Track CPU-per-request metrics on Ruby services that consume ruby_llm and baseline against typical response processing times.
  • Enable rate-limit and timeout alerts for chat-completion endpoints to catch amplification patterns.
  • Capture and retain anomalous model responses for offline analysis when parser latency alerts fire.

How to Mitigate CVE-2026-67987

Immediate Actions Required

  • Upgrade ruby_llm past commit fa6f279847d6d7027814539d9c0dfc3bbdfd2a83 to a release containing commit 5e88411f171721b381853fa77d254e266dcf6ad8.
  • Apply request-level timeouts around any code path that parses model responses.
  • Audit application logs for prior chat-completion latency anomalies that may indicate exploitation attempts.

Patch Information

The upstream fix is GitHub Commit 5e88411, which removes <think> tag parsing from string assistant content in lib/ruby_llm/protocols/chat_completions/chat.rb and strips the @inside_think_tag and @pending_think_tag state from lib/ruby_llm/stream_accumulator.rb. Review the affected code snippet in Chat.rb and the ruby_llm repository for release details.

Workarounds

  • Wrap chat-completion parsing in a Timeout.timeout block with a strict upper bound while the patch is deployed.
  • Reject or truncate assistant responses whose length or <think> tag count exceeds a defined threshold before handing them to the parser.
  • Route model responses through an intermediary sanitization step that strips unmatched <think> tags.
bash
# Configuration example: pin ruby_llm to a patched revision in Gemfile
bundle update ruby_llm
bundle info ruby_llm | grep -i revision
# Verify the deployed commit hash is at or after 5e88411f171721b381853fa77d254e266dcf6ad8

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.