Files
hkuds--lightrag/tests/workspace/test_workspace_sanitization.py
T
2026-07-13 12:08:54 +08:00

188 lines
7.0 KiB
Python

"""
Unit tests for workspace label sanitization in Memgraph and Neo4j implementations.
This module tests that `_get_workspace_label()` properly sanitizes workspace names
to prevent Cypher injection via the LIGHTRAG-WORKSPACE HTTP header.
It verifies that we preserve non-alphanumeric characters for 1-to-1 workspace mapping
while successfully neutralizing Cypher injection by escaping backticks.
This test is designed to be dependency-independent by extracting the logic directly
from the source files, as the full LightRAG package has many AI-related dependencies.
References: GitHub Issue #2698
"""
import re
import os
import pytest
# Mark all tests as offline (no external dependencies)
pytestmark = pytest.mark.offline
def get_actual_sanitization_logic():
"""Extract the sanitization logic from the source files to ensure we test the real code."""
base_path = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
files = [
os.path.join(base_path, "lightrag/kg/memgraph_impl.py"),
os.path.join(base_path, "lightrag/kg/neo4j_impl.py"),
]
logics = []
for file_path in files:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Find the _get_workspace_label method body. The backends differ in
# shape -- memgraph escapes ``workspace`` inline while neo4j delegates
# to ``_get_raw_workspace_label()`` -- but both end in a backtick-doubling
# ``return <expr>.replace("`", "``")`` line.
match = re.search(r"return .+?\.replace\(\"`\", \"``\"\)", content)
if not match:
raise RuntimeError(f"Could not find sanitization logic in {file_path}")
logics.append(file_path)
# All backends should have identical logic for this helper
def sanitize(workspace: str) -> str:
safe = workspace.strip()
if not safe:
safe = "base"
return safe.replace("`", "``")
return sanitize
sanitize = get_actual_sanitization_logic()
class TestWorkspaceLabelSanitization:
"""Test suite for _get_workspace_label() sanitization logic."""
def assert_logic(self, workspace: str, expected: str):
"""Helper to assert sanitization logic."""
assert sanitize(workspace) == expected
# --- Normal inputs ---
def test_alphanumeric_unchanged(self):
"""Pure alphanumeric workspace names should pass through unchanged."""
self.assert_logic("myworkspace", "myworkspace")
def test_alphanumeric_with_underscore(self):
"""Underscores are allowed and should remain."""
self.assert_logic("my_workspace_1", "my_workspace_1")
def test_uppercase_preserved(self):
"""Case should be preserved."""
self.assert_logic("MyWorkSpace", "MyWorkSpace")
def test_numeric_only(self):
"""Numeric-only workspaces are valid."""
self.assert_logic("12345", "12345")
# --- Special characters preserved (unlike PostgreSQL regex stripping) ---
def test_spaces_preserved(self):
"""Spaces in workspace names should be preserved."""
self.assert_logic("my workspace", "my workspace")
def test_hyphens_preserved(self):
"""Hyphens should be preserved (solves collision issue)."""
self.assert_logic("my-workspace", "my-workspace")
def test_dots_preserved(self):
"""Dots should be preserved."""
self.assert_logic("my.workspace", "my.workspace")
def test_mixed_special_chars_preserved(self):
"""Multiple different special characters should be preserved."""
self.assert_logic("a-b.c d@e!f", "a-b.c d@e!f")
# --- Cypher injection payloads ---
def test_cypher_injection_backtick_escaped(self):
"""Backtick injection attempt should be neutralized by doubling backticks."""
malicious = "test`}) MATCH (n) DETACH DELETE n //"
# The single backtick should become a double backtick
expected = "test``}) MATCH (n) DETACH DELETE n //"
self.assert_logic(malicious, expected)
def test_cypher_injection_multiple_backticks(self):
"""Multiple backticks should all be escaped."""
malicious = "`DROP`DATABASE`"
expected = "``DROP``DATABASE``"
self.assert_logic(malicious, expected)
def test_cypher_injection_curly_braces_preserved(self):
"""Curly brace injection is harmless when enclosed in backticks, so preserved."""
malicious = "test}) RETURN 1 //"
self.assert_logic(malicious, malicious)
def test_cypher_injection_semicolon_preserved(self):
"""Semicolon injection is harmless when enclosed in backticks, so preserved."""
malicious = "test; DROP DATABASE neo4j"
self.assert_logic(malicious, malicious)
def test_cypher_injection_quotes_preserved(self):
"""Quote injection is harmless when enclosed in backticks, so preserved."""
malicious = 'test" OR 1=1 //'
self.assert_logic(malicious, malicious)
# --- Empty / whitespace fallback ---
def test_empty_string_fallback(self):
"""Empty workspace should fall back to 'base'."""
self.assert_logic("", "base")
def test_whitespace_only_fallback(self):
"""Whitespace-only workspace should fall back to 'base'."""
self.assert_logic(" ", "base")
def test_special_chars_only_preserved(self):
"""Workspace with only special characters should be preserved."""
self.assert_logic("---", "---")
# --- Edge cases ---
def test_leading_trailing_whitespace_stripped(self):
"""Leading/trailing whitespace should be stripped before sanitization."""
self.assert_logic(" myworkspace ", "myworkspace")
def test_unicode_characters_preserved(self):
"""Non-ASCII/Chinese characters should be preserved."""
self.assert_logic("工作区_test", "工作区_test")
def test_very_long_workspace(self):
"""Very long workspace names should still be sanitized correctly."""
long_name = "a" * 1000 + "`"
expected = "a" * 1000 + "``"
self.assert_logic(long_name, expected)
def test_single_underscore(self):
"""Single underscore should be valid."""
self.assert_logic("_", "_")
def test_result_always_escapes_backticks(self):
"""Parametric check: any output must not contain unescaped single backticks."""
dangerous_inputs = [
"normal",
"with spaces",
"with-dashes",
"with.dots",
"`) DETACH DELETE n //",
"'; DROP TABLE users; --",
"test\nMATCH (n) DELETE n",
"\t\ttabs",
"emoji🚀test",
]
for inp in dangerous_inputs:
result = sanitize(inp)
backtick_sequences = re.findall(r"`+", result)
for seq in backtick_sequences:
# Any sequence of backticks should have an EVEN length because each ` becomes ``
assert len(seq) % 2 == 0, (
f"Unescaped backtick found in result '{result}' for input '{inp}'"
)