Skip to content

ReDoS in substitute() via :s command — user-supplied regex causes catastrophic backtracking (CWE-1333) #159

Description

@gnsehfvlr

Summary

pyvim's :s (substitute) command passes the user-typed search pattern directly to re.sub() without any validation. A user can type a catastrophic regex pattern (e.g. (a+)+$) that causes Python's regex engine to backtrack exponentially, freezing the editor process indefinitely.

Affected Version

pyvim 3.0.3 (latest)

Vulnerable Code

pyvim/commands/commands.py, line 723–726:

def get_transform_callback(search, replace, flags):
    SUBSTITUTE_ALL, SUBSTITUTE_ONE = 0, 1
    sub_count = SUBSTITUTE_ALL if 'g' in flags else SUBSTITUTE_ONE
    return lambda s: re.sub(search, replace, s, count=sub_count)  # ← search is raw user input

pyvim/commands/handler.py, line 33–35:

elif command in ('s', 'substitute'):
    flags = flags.lstrip('/')
    substitute(editor, range_start, range_end, search, replace, flags)

The search variable is parsed directly from what the user types in the : command line and passed to re.sub() with no validation or timeout.

Root Cause

re.sub() in Python's re module uses a backtracking NFA engine that exhibits exponential time complexity for certain regex patterns (nested quantifiers, alternation) on carefully crafted input. No timeout or safe-regex check is applied before executing the substitution.

Proof of Concept

Open pyvim and enter any file with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (30+ a characters) on a line. Then type the substitute command:

:s/(a+)+$/x

Measured freeze times (Python re.sub benchmark):

Input length Time
20 as 0.09s
25 as 2.73s
30 as 88s
35 as >1000s (estimated)

The editor process freezes completely and becomes unresponsive.

Other catastrophic patterns:

  • ([a-zA-Z]+)*$
  • (a|a?)+$
  • (\w+\s?)+$

Impact

Any user of pyvim can accidentally (or intentionally) freeze their editor session by typing a substitute command with an ambiguous regex pattern. In a shared environment (e.g. a pyvim-based coding tool), this constitutes a local denial of service.

CVSS Score

5.5 MEDIUMCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Suggested Fix

Use the re2 library (linear-time regex engine) or apply a per-call timeout using signal.alarm / a thread-based timeout:

import re2  # linear-time, no backtracking
return lambda s: re2.sub(search, replace, s, count=sub_count)

Or validate the pattern before use:

import re2
def get_transform_callback(search, replace, flags):
    try:
        re2.compile(search)  # raises if pattern is unsafe/complex
    except re2.error:
        raise ValueError(f"Unsafe regex pattern: {search}")
    ...

Reporter

Younghun Lee — PYPI supply-chain vulnerability research

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions