Module: Alap::ValidateRegex

Defined in:
lib/alap/validate_regex.rb

Overview

Lightweight ReDoS guard for server-side regex parameters.

Rejects patterns with nested quantifiers that cause catastrophic backtracking: (a+)+, (a*)*b, (w+w+)+, etc.

Constant Summary collapse

QUANTIFIER_AFTER =
/\A(?:[?*+]|\{\d+(?:,\d*)?\})/
QUANTIFIER_IN_BODY =
/[?*+]|\{\d+(?:,\d*)?\}/

Class Method Summary collapse

Class Method Details

.call(pattern) ⇒ Object

Returns { “safe” => true } or { “safe” => false, “reason” => “…” }.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/alap/validate_regex.rb', line 17

def self.call(pattern)
  begin
    Regexp.new(pattern)
  rescue RegexpError
    return { "safe" => false, "reason" => "Invalid regex syntax" }
  end

  group_starts = []
  i = 0

  while i < pattern.length
    ch = pattern[i]

    # Skip escaped characters
    if ch == "\\"
      i += 2
      next
    end

    # Skip character classes [...]
    if ch == "["
      i += 1
      i += 1 if i < pattern.length && pattern[i] == "^"
      i += 1 if i < pattern.length && pattern[i] == "]"
      while i < pattern.length && pattern[i] != "]"
        i += 1 if pattern[i] == "\\"
        i += 1
      end
      i += 1
      next
    end

    if ch == "("
      group_starts.push(i)
      i += 1
      next
    end

    if ch == ")"
      unless group_starts.empty?
        start = group_starts.pop
        after_group = pattern[(i + 1)..]
        if after_group && QUANTIFIER_AFTER.match?(after_group)
          body = pattern[(start + 1)...i]
          stripped = strip_escapes_and_classes(body)
          if QUANTIFIER_IN_BODY.match?(stripped)
            return { "safe" => false, "reason" => "Nested quantifier detected — potential ReDoS" }
          end
        end
      end
      i += 1
      next
    end

    i += 1
  end

  { "safe" => true }
end