Skip to content

Prevent ReDoS in SSTI validation patterns#2516

Merged
crivetimihai merged 4 commits intomainfrom
fix/security-redos-ssti-patterns-2366
Feb 1, 2026
Merged

Prevent ReDoS in SSTI validation patterns#2516
crivetimihai merged 4 commits intomainfrom
fix/security-redos-ssti-patterns-2366

Conversation

@shoummu1
Copy link
Copy Markdown
Collaborator

🐛 Bug-fix PR

📌 Summary

Fixes ReDoS (Regular Expression Denial of Service) vulnerability in Server-Side Template Injection (SSTI) validation patterns that could allow attackers to cause CPU exhaustion and service timeouts through crafted input.

🔗 Related Issue

Closes #2366

🔁 Reproduction Steps

The vulnerability could be triggered by:

  1. Submitting malformed template input like {{aaaa...aaaa (unclosed brackets) to any validation endpoint
  2. Input length up to 64KB (MAX_TEMPLATE_LENGTH = 65536) would cause catastrophic backtracking
  3. Results in O(n²) or worse time complexity, causing high CPU usage and request timeouts

🐞 Root Cause

The _SSTI_PATTERNS list in mcpgateway/common/validators.py (lines 95-103) used greedy quantifiers .* in regex patterns like:

re.compile(r"\{\{.*(__|\.|config).*\}\}", re.IGNORECASE)

When given input without closing delimiters (e.g., {{aaaa...aaaa), the regex engine would:

  1. Match .* greedily to the end
  2. Fail to find closing }}
  3. Backtrack one character at a time
  4. Retry at each position, causing exponential time complexity

💡 Fix Description

Replaced all vulnerable .* patterns with negated character classes that cannot backtrack:

Before:

re.compile(r"\{\{.*(__|\.|config|...).*\}\}", re.IGNORECASE)

After:

re.compile(r"\{\{[^}]*(__|\.|config|...)[^}]*\}\}", re.IGNORECASE)

Key changes:

  • [^}]* - matches any character except }, bounded by delimiter
  • [^%]* - matches any character except %, bounded by delimiter
  • Eliminates backtracking while maintaining identical detection behavior
  • All 9 SSTI patterns updated for consistency

The negated character classes provide linear time complexity O(n) instead of exponential, preventing the ReDoS attack vector.

🧪 Verification

Check Command Status
Lint suite make lint
Unit tests make test
Coverage ≥ 90 % make coverage
Security validation tests pytest tests/security/test_input_validation.py ✅ 58 passed
SSTI pattern detection All dangerous payloads still detected

Test Results:

  • test_server_side_template_injection: ✅ All 8 dangerous SSTI payloads correctly rejected
  • test_regex_dos_prevention: ✅ Passes with ReDoS timing checks
  • All 58 security validation tests: ✅ Pass
  • All 72 validator unit tests: ✅ Pass

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients
  • Internal security hardening only - no API changes

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed
  • All existing tests pass
  • Security vulnerability mitigated

@shoummu1 shoummu1 marked this pull request as ready for review January 27, 2026 11:23
@shoummu1 shoummu1 force-pushed the fix/security-redos-ssti-patterns-2366 branch from 382cbf6 to 9383bd4 Compare January 27, 2026 13:50
@crivetimihai crivetimihai force-pushed the fix/security-redos-ssti-patterns-2366 branch 8 times, most recently from 24d9f7c to 6eff6e7 Compare January 28, 2026 00:15
@crivetimihai
Copy link
Copy Markdown
Member

@shoummu1 I've made some updates, please review / retest

@crivetimihai
Copy link
Copy Markdown
Member

Summary of Changes

This PR has been significantly refactored to use a manual parser approach instead of regex patterns for SSTI detection. This eliminates the ReDoS vulnerability while improving bypass resistance.

Implementation Changes

Replaced regex patterns with linear-time parser:

Before (Regex) After (Manual Parser)
\{\{.*(__|.|config|...).*\}\} with .* causing backtracking _iter_template_expressions() - O(n) linear scan
Vulnerable to ReDoS with pathological input No backtracking possible
Bypassed by }} inside quoted strings Properly tracks quote state and escapes

New functions added to validators.py:

  • _iter_template_expressions(value, start, end) - Yields template expression contents while correctly handling:
    • Single and double quoted strings
    • Escaped characters (\", \')
    • Nested delimiters inside quotes (e.g., "}}" in strings)
  • _has_simple_template_expression(value, start) - Fast check for ${, #{, %{ patterns

New constants:

  • _SSTI_DANGEROUS_SUBSTRINGS - Keywords to detect: __, ., config, self, request, application, globals, builtins, import
  • _SSTI_DANGEROUS_OPERATORS - Arithmetic operators for {{ }}: *, /, +, -
  • _SSTI_SIMPLE_TEMPLATE_PREFIXES - Simple template prefixes: ${, #{, %{

Bypass Resistance

Attack Vector Status
{{ "}}" ~ self.__class__ }} ✅ Blocked
{{ 'a\'}}b' ~ self }} ✅ Blocked
{{ "a\"}}b" ~ self }} ✅ Blocked
`{{ '' attr('class') }}`
`{{ '' attr('' ~ 'class') }}`
{{ '}' + self.__class__ }} ✅ Blocked
`{{ users map(attribute='class') }}`

Pre-existing Limitation

The fully-split dunder bypass ({{ ''|attr('_' ~ '_' ~ 'class' ~ '_' ~ '_') }}) is also allowed on main - this is a fundamental limitation of static analysis without template execution, not a regression from this PR.

Test Coverage

  • Added comprehensive SSTI bypass test cases covering all vectors above
  • Added @pytest.mark.timeout(30) for deterministic ReDoS detection
  • Added pathological input tests (10,000 character strings with unclosed delimiters)
  • All 58 security tests pass

Performance

  • Before: Regex .* patterns could cause exponential backtracking on malicious input
  • After: Linear O(n) scanning regardless of input - no backtracking possible

@shoummu1 shoummu1 force-pushed the fix/security-redos-ssti-patterns-2366 branch from 62ed8a9 to 040b8a9 Compare January 28, 2026 07:05
@shoummu1
Copy link
Copy Markdown
Collaborator Author

@crivetimihai I've reviewed and tested the refactored implementation.

Test Results

  • ✅ All 31 security validation tests passing
  • test_server_side_template_injection: All 34 SSTI payloads correctly detected (including 26 new bypass tests)
  • test_regex_dos_prevention: Completes quickly with pathological inputs (10K chars)
  • ✅ No regressions in existing functionality

Implementation Verification

Parser functions present and correct:

  • _iter_template_expressions(): State machine with proper quote tracking and escape handling
  • _has_simple_template_expression(): Fast check for ${, #{, %{ patterns

Regex patterns successfully replaced with:

  • _SSTI_DANGEROUS_SUBSTRINGS: Keyword detection
  • _SSTI_DANGEROUS_OPERATORS: Arithmetic operator detection
  • _SSTI_SIMPLE_TEMPLATE_PREFIXES: Simple template patterns

Bypass resistance verified:

  • {{ "}}" ~ self.__class__ }} - Blocked
  • {{ "a\\"}}b" ~ self }} - Blocked
  • {{ ''|attr('__class__') }} - Blocked

Security Improvements Confirmed

  1. ReDoS eliminated: O(n) linear scanning, no catastrophic backtracking possible
  2. Quote handling: Parser correctly tracks single/double quotes and escapes
  3. Bypass resistance: 425% increase in test coverage (8→34 cases)

@crivetimihai crivetimihai requested a review from jonpspri January 28, 2026 10:01
@shoummu1 shoummu1 force-pushed the fix/security-redos-ssti-patterns-2366 branch 2 times, most recently from 3a8f8b2 to d76fcaf Compare January 30, 2026 09:02
Replace regex-based SSTI detection with a linear-time manual parser
to eliminate ReDoS vulnerability while improving bypass resistance.

Changes:
- Add _iter_template_expressions() parser that correctly handles:
  - Quoted strings (single and double quotes)
  - Escaped characters within strings
  - Nested delimiters inside quotes (e.g., "}}" in strings)
  - Continues scanning after unterminated expressions (fail-closed)
- Replace _SSTI_PATTERNS regex list with:
  - _SSTI_DANGEROUS_SUBSTRINGS tuple for keyword detection
  - _SSTI_DANGEROUS_OPERATORS tuple for arithmetic in {{ }} and {% %}
  - _SSTI_SIMPLE_TEMPLATE_PREFIXES for ${, #{, %{ expressions
- Add _has_simple_template_expression() with O(n) linear scan using rfind
- Fix type annotation for validate_parameter_length()
- Block dynamic attribute access bypasses:
  - |attr filter for dynamic attribute access (with whitespace normalization)
  - |selectattr, |sort, |map filters (can take attribute names)
  - getattr function
  - ~ operator for string concatenation (dunder name construction)
  - [ bracket notation for dynamic access
  - % operator for string formatting (e.g., '%c' % 95)
  - attribute= parameter (blocks map/selectattr/sort attribute access)
  - All escape sequences: \x, \u, \N{, \0-\7 (octal)
- Apply operator checks to both {{ }} and {% %} blocks
- Normalize whitespace around | and = before checking

Performance:
- O(n) linear scanning eliminates catastrophic backtracking
- _has_simple_template_expression uses rfind for O(n) instead of O(n²)

Security:
- Proper quote handling blocks bypasses like {{ "}}" ~ self.__class__ }}
- Escaped quote handling blocks {{ "a\"}}b" ~ self }} bypasses
- Blocks dynamic construction bypasses via string concatenation
- Blocks all escape sequence bypasses (hex, unicode, octal)
- Blocks whitespace-based bypasses around | and =
- Blocks % formatting bypasses (e.g., '%c%c' % (95,95))
- Fail-closed: continues scanning after unterminated expressions

Tests:
- Add comprehensive SSTI bypass test cases
- Add pytest.mark.timeout(30) for deterministic ReDoS detection
- Add pathological input tests for ReDoS prevention verification

Closes #2366

Co-authored-by: Shoumi <shoumimukherjee@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai crivetimihai force-pushed the fix/security-redos-ssti-patterns-2366 branch from d76fcaf to d69cea0 Compare January 31, 2026 23:32
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
- Raise ValueError immediately on unterminated {{ or {% expressions
- Eliminates O(n²) rescan path, restoring O(n) worst-case performance
- Use consistent error message with other validation failures
- Add regression test for unterminated expression rejection

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Changes Summary

This PR has been rebased onto main and includes the following security hardening:

ReDoS Prevention

  • Replaced regex-based SSTI detection with a linear-time manual parser (_iter_template_expressions())
  • Original patterns used .* which could cause catastrophic backtracking
  • New implementation is O(n) with proper quote-awareness

SSTI Bypass Vectors Blocked

  • Dynamic construction: Added ~ operator and |attr to blocklists
  • Escape sequences: Block \x (hex), \u (unicode), \N{ (named), \0-\7 (octal)
  • String formatting: Block % operator (prevents '%c' % 95 bypasses)
  • Filter bypasses: Block |selectattr, |sort, |map and attribute=
  • Whitespace normalization: Normalize around | and = to prevent | attr bypasses

Performance

  • O(n) complexity in _has_simple_template_expression using rfind
  • O(n) complexity in _iter_template_expressions with fail-closed on unterminated expressions

Fail-Closed Behavior

  • Unterminated template expressions ({{ without }}) now raise ValueError immediately
  • Prevents potential O(n²) rescanning and ensures malformed input is rejected

Tests

  • Added comprehensive test payloads for all bypass vectors
  • Added @pytest.mark.timeout(30) for ReDoS detection
  • Added regression test for unterminated expressions

Other Fixes

  • Fixed pre-existing type annotation bug: max_length: int = NoneOptional[int] = None

Move ValueError documentation to proper Raises: section format.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai crivetimihai merged commit 0aed3c4 into main Feb 1, 2026
51 checks passed
@crivetimihai crivetimihai deleted the fix/security-redos-ssti-patterns-2366 branch February 1, 2026 00:14
hughhennelly pushed a commit to hughhennelly/mcp-context-forge that referenced this pull request Feb 8, 2026
* fix: prevent ReDoS in SSTI validation patterns

Replace regex-based SSTI detection with a linear-time manual parser
to eliminate ReDoS vulnerability while improving bypass resistance.

Changes:
- Add _iter_template_expressions() parser that correctly handles:
  - Quoted strings (single and double quotes)
  - Escaped characters within strings
  - Nested delimiters inside quotes (e.g., "}}" in strings)
  - Continues scanning after unterminated expressions (fail-closed)
- Replace _SSTI_PATTERNS regex list with:
  - _SSTI_DANGEROUS_SUBSTRINGS tuple for keyword detection
  - _SSTI_DANGEROUS_OPERATORS tuple for arithmetic in {{ }} and {% %}
  - _SSTI_SIMPLE_TEMPLATE_PREFIXES for ${, #{, %{ expressions
- Add _has_simple_template_expression() with O(n) linear scan using rfind
- Fix type annotation for validate_parameter_length()
- Block dynamic attribute access bypasses:
  - |attr filter for dynamic attribute access (with whitespace normalization)
  - |selectattr, |sort, |map filters (can take attribute names)
  - getattr function
  - ~ operator for string concatenation (dunder name construction)
  - [ bracket notation for dynamic access
  - % operator for string formatting (e.g., '%c' % 95)
  - attribute= parameter (blocks map/selectattr/sort attribute access)
  - All escape sequences: \x, \u, \N{, \0-\7 (octal)
- Apply operator checks to both {{ }} and {% %} blocks
- Normalize whitespace around | and = before checking

Performance:
- O(n) linear scanning eliminates catastrophic backtracking
- _has_simple_template_expression uses rfind for O(n) instead of O(n²)

Security:
- Proper quote handling blocks bypasses like {{ "}}" ~ self.__class__ }}
- Escaped quote handling blocks {{ "a\"}}b" ~ self }} bypasses
- Blocks dynamic construction bypasses via string concatenation
- Blocks all escape sequence bypasses (hex, unicode, octal)
- Blocks whitespace-based bypasses around | and =
- Blocks % formatting bypasses (e.g., '%c%c' % (95,95))
- Fail-closed: continues scanning after unterminated expressions

Tests:
- Add comprehensive SSTI bypass test cases
- Add pytest.mark.timeout(30) for deterministic ReDoS detection
- Add pathological input tests for ReDoS prevention verification

Closes IBM#2366

Co-authored-by: Shoumi <shoumimukherjee@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* lint

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: enforce true fail-closed on unterminated template expressions

- Raise ValueError immediately on unterminated {{ or {% expressions
- Eliminates O(n²) rescan path, restoring O(n) worst-case performance
- Use consistent error message with other validation failures
- Add regression test for unterminated expression rejection

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: add proper Raises section to docstring for darglint

Move ValueError documentation to proper Raises: section format.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: hughhennnelly <hughhennelly06@gmail.com>
kcostell06 pushed a commit to kcostell06/mcp-context-forge that referenced this pull request Feb 24, 2026
* fix: prevent ReDoS in SSTI validation patterns

Replace regex-based SSTI detection with a linear-time manual parser
to eliminate ReDoS vulnerability while improving bypass resistance.

Changes:
- Add _iter_template_expressions() parser that correctly handles:
  - Quoted strings (single and double quotes)
  - Escaped characters within strings
  - Nested delimiters inside quotes (e.g., "}}" in strings)
  - Continues scanning after unterminated expressions (fail-closed)
- Replace _SSTI_PATTERNS regex list with:
  - _SSTI_DANGEROUS_SUBSTRINGS tuple for keyword detection
  - _SSTI_DANGEROUS_OPERATORS tuple for arithmetic in {{ }} and {% %}
  - _SSTI_SIMPLE_TEMPLATE_PREFIXES for ${, #{, %{ expressions
- Add _has_simple_template_expression() with O(n) linear scan using rfind
- Fix type annotation for validate_parameter_length()
- Block dynamic attribute access bypasses:
  - |attr filter for dynamic attribute access (with whitespace normalization)
  - |selectattr, |sort, |map filters (can take attribute names)
  - getattr function
  - ~ operator for string concatenation (dunder name construction)
  - [ bracket notation for dynamic access
  - % operator for string formatting (e.g., '%c' % 95)
  - attribute= parameter (blocks map/selectattr/sort attribute access)
  - All escape sequences: \x, \u, \N{, \0-\7 (octal)
- Apply operator checks to both {{ }} and {% %} blocks
- Normalize whitespace around | and = before checking

Performance:
- O(n) linear scanning eliminates catastrophic backtracking
- _has_simple_template_expression uses rfind for O(n) instead of O(n²)

Security:
- Proper quote handling blocks bypasses like {{ "}}" ~ self.__class__ }}
- Escaped quote handling blocks {{ "a\"}}b" ~ self }} bypasses
- Blocks dynamic construction bypasses via string concatenation
- Blocks all escape sequence bypasses (hex, unicode, octal)
- Blocks whitespace-based bypasses around | and =
- Blocks % formatting bypasses (e.g., '%c%c' % (95,95))
- Fail-closed: continues scanning after unterminated expressions

Tests:
- Add comprehensive SSTI bypass test cases
- Add pytest.mark.timeout(30) for deterministic ReDoS detection
- Add pathological input tests for ReDoS prevention verification

Closes IBM#2366

Co-authored-by: Shoumi <shoumimukherjee@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* lint

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: enforce true fail-closed on unterminated template expressions

- Raise ValueError immediately on unterminated {{ or {% expressions
- Eliminates O(n²) rescan path, restoring O(n) worst-case performance
- Use consistent error message with other validation failures
- Add regression test for unterminated expression rejection

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: add proper Raises section to docstring for darglint

Move ValueError documentation to proper Raises: section format.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SECURITY][SONAR][MEDIUM]: ReDoS vulnerability in SSTI validation patterns in validators.py

2 participants