Skip to content

ReDoS in gitignore regex translation (default Python re backend) via crafted ** chains #131

Description

@befeleme

I have read the security policy and know you're not considering this type of issues a security vulnerability of pathspec. It is still a valid issue though and could be easy to mitigate, so I decided to report as a regular issue. Also happy to send the PR if you wish to fix it the proposed way.
The issue has been found by AISLE in partnership with Red Hat, reported by AI, verified by me.

When using the default Python re backend, it's easy to craft a chain of ** entries in gitignore, leading to ReDoS.

Reproducer (notice the increasing execution time in the output), needs to be run in a default environment without google-re2 or hyperscan:

import time, re
from pathspec import PathSpec, GitIgnoreSpec
from pathspec.patterns.gitignore.spec import GitIgnoreSpecPattern

print(type(PathSpec.from_lines("gitignore", ["a"])._backend).__name__)
print(type(GitIgnoreSpec.from_lines(["a"])._backend).__name__)

def test(m):
    pat = '/'.join(['a'] + sum((['**', 'b'] for _ in range(m)), []) + ['c'])
    s = '/'.join(['a'] + ['b']*(m*2+4))
    rx, _ = GitIgnoreSpecPattern.pattern_to_regex(pat)
    r = re.compile(rx)
    t0 = time.perf_counter(); r.search(s); return time.perf_counter() - t0

for m in [10, 11, 12]:
    print(m, test(m))

On my machine the output looks ~this:

10 0.23174957901937887
11 0.9534206030075438
12 3.9229953379835933

A solution could be to reject unusually complex patterns before regex generation in both pathspec/patterns/gitignore/spec.py and pathspec/patterns/gitignore/basic.py, along the lines of:

class GitIgnoreSpecPattern(_GitIgnoreBasePattern):
# class GitIgnoreBasicPattern(_GitIgnoreBasePattern) respectively
+    _MAX_PATTERN_LENGTH = 4096
+    _MAX_DOUBLESTAR_SEGMENTS = 8
@@

# Split pattern into segments.
+        if len(pattern_str) > cls._MAX_PATTERN_LENGTH:
+            raise GitIgnorePatternError(f"Pattern too long: {len(pattern_str)}")
+        # Split pattern into segments.
          pattern_segs = pattern_str.split('/')
+        if sum(1 for seg in pattern_segs if seg == '**') > cls._MAX_DOUBLESTAR_SEGMENTS:
+            raise GitIgnorePatternError("Pattern has too many '**' segments.")

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions