Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions tests/entrypoints/serve/utils/test_api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,57 @@ def test_strips_both_address_and_path(self):
result = sanitize_message(msg)
assert "0x" not in result
assert "/usr/local/" not in result


class TestSanitizeMessageReDoSResistance:
"""Regression tests for GHSA-f2g9-pmwr-xwc7.

Verifies that sanitize_message performs bounded near-linear work
on adversarial slash-delimited input with no filename extension.
"""

def test_slash_segments_no_extension_completes_quickly(self):
"""50k slash-segments without dot-extension must complete in < 0.1s."""
import time

message = "/a" * 50000
t0 = time.perf_counter()
result = sanitize_message(message)
elapsed = time.perf_counter() - t0
assert elapsed < 0.1, f"Took {elapsed:.2f}s (expected < 0.1s)"
assert result == message

def test_alphanumeric_control_completes(self):
"""100k alphanumeric chars must complete near-instantly."""
import time

message = "a" * 100000
t0 = time.perf_counter()
result = sanitize_message(message)
elapsed = time.perf_counter() - t0
assert elapsed < 0.1, f"Took {elapsed:.2f}s"
assert result == message

def test_linear_scaling(self):
"""Verify near-linear scaling across input sizes."""
import time

times = []
for n in [1000, 10000, 50000]:
message = "/a" * n
t0 = time.perf_counter()
sanitize_message(message)
times.append(time.perf_counter() - t0)

ratio = times[-1] / max(times[0], 1e-9)
assert ratio < 100, (
f"50k/1k time ratio is {ratio:.0f}x (expected < 100x for linear)"
)

def test_path_redaction_still_works(self):
"""Legitimate paths are still redacted after the fix."""
assert "<path>" in sanitize_message("Error in /app/server.py")
assert "<path>" in sanitize_message("at /workspace/vllm/engine.py")
assert "<path>" in sanitize_message(
"error at /usr/lib/python3.12/dist-packages/vllm/core.py"
)
3 changes: 2 additions & 1 deletion vllm/entrypoints/serve/utils/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ def sanitize_message(message: str) -> str:
message = re.sub(
r"/(?:home|usr|opt|var|tmp|root|lib|mnt|srv)(?:/[\w.\-]+)+", "<path>", message
)
message = re.sub(r"(?:/[\w\-]+)+/[\w\-]+\.\w+", "<path>", message)
if "." in message:
message = re.sub(r"(?>/[\w\-]+)+/[\w\-]+\.\w+", "<path>", message)
Comment on lines +319 to +320

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

The atomic group (?>...) prevents exponential backtracking but still exhibits O(n²) behavior: re.sub retries the pattern at every /-position, each time the atomic group scans the remaining input. Input like "/a" * 5000 + " error.txt" (dot bypasses the pre-check) takes ~9 seconds; at 50k segments it would block a worker for minutes. The original advisory PoC shape is only partially mitigated.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Add a message length guard to the if condition on line 319 to bound the O(n²) behavior. The re.sub retries the atomic-group pattern at every /-position, each scanning forward through the remaining input, creating quadratic time complexity. By capping the message length for this specific regex (e.g., len(message) <= 2048), the worst-case time is bounded to ~30ms. Legitimate error messages with file paths are well under 2KB, and the preceding known-root-directory regex on lines 316-318 already handles common paths (/home/..., /usr/..., etc.) without this quadratic issue. Alternatively, for a more comprehensive fix, consider truncating the message at the top of sanitize_message() (e.g., message = message[:4096]) to protect all regexes in the function, or replace the regex with a linear-time token-based approach.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
if "." in message:
message = re.sub(r"(?>/[\w\-]+)+/[\w\-]+\.\w+", "<path>", message)
if "." in message and len(message) <= 2048:
message = re.sub(r"(?>/[\w\-]+)+/[\w\-]+\.\w+", "<path>", message)

return message.strip()


Expand Down
Loading