forked from cardosofelipe/fast-next-template
security(memory): escape SQL ILIKE patterns to prevent injection
- Add _escape_like_pattern() helper to escape SQL wildcards (%, _, \) - Apply escaping in SemanticMemory.search_facts and get_by_entity - Apply escaping in ProceduralMemory.search and find_best_for_task Prevents attackers from injecting SQL wildcard patterns through user-controlled search terms. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,25 @@ from app.services.memory.types import Procedure, ProcedureCreate, RetrievalResul
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_like_pattern(pattern: str) -> str:
|
||||||
|
"""
|
||||||
|
Escape SQL LIKE/ILIKE special characters to prevent pattern injection.
|
||||||
|
|
||||||
|
Characters escaped:
|
||||||
|
- % (matches zero or more characters)
|
||||||
|
- _ (matches exactly one character)
|
||||||
|
- \\ (escape character itself)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pattern: Raw search pattern from user input
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Escaped pattern safe for use in LIKE/ILIKE queries
|
||||||
|
"""
|
||||||
|
# Escape backslash first, then the wildcards
|
||||||
|
return pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||||
|
|
||||||
|
|
||||||
def _model_to_procedure(model: ProcedureModel) -> Procedure:
|
def _model_to_procedure(model: ProcedureModel) -> Procedure:
|
||||||
"""Convert SQLAlchemy model to Procedure dataclass."""
|
"""Convert SQLAlchemy model to Procedure dataclass."""
|
||||||
return Procedure(
|
return Procedure(
|
||||||
@@ -320,7 +339,9 @@ class ProceduralMemory:
|
|||||||
if search_terms:
|
if search_terms:
|
||||||
conditions = []
|
conditions = []
|
||||||
for term in search_terms:
|
for term in search_terms:
|
||||||
term_pattern = f"%{term}%"
|
# Escape SQL wildcards to prevent pattern injection
|
||||||
|
escaped_term = _escape_like_pattern(term)
|
||||||
|
term_pattern = f"%{escaped_term}%"
|
||||||
conditions.append(
|
conditions.append(
|
||||||
or_(
|
or_(
|
||||||
ProcedureModel.trigger_pattern.ilike(term_pattern),
|
ProcedureModel.trigger_pattern.ilike(term_pattern),
|
||||||
@@ -368,6 +389,10 @@ class ProceduralMemory:
|
|||||||
Returns:
|
Returns:
|
||||||
Best matching procedure or None
|
Best matching procedure or None
|
||||||
"""
|
"""
|
||||||
|
# Escape SQL wildcards to prevent pattern injection
|
||||||
|
escaped_task_type = _escape_like_pattern(task_type)
|
||||||
|
task_type_pattern = f"%{escaped_task_type}%"
|
||||||
|
|
||||||
# Build query for procedures matching task type
|
# Build query for procedures matching task type
|
||||||
stmt = (
|
stmt = (
|
||||||
select(ProcedureModel)
|
select(ProcedureModel)
|
||||||
@@ -376,8 +401,8 @@ class ProceduralMemory:
|
|||||||
(ProcedureModel.success_count + ProcedureModel.failure_count)
|
(ProcedureModel.success_count + ProcedureModel.failure_count)
|
||||||
>= min_uses,
|
>= min_uses,
|
||||||
or_(
|
or_(
|
||||||
ProcedureModel.trigger_pattern.ilike(f"%{task_type}%"),
|
ProcedureModel.trigger_pattern.ilike(task_type_pattern),
|
||||||
ProcedureModel.name.ilike(f"%{task_type}%"),
|
ProcedureModel.name.ilike(task_type_pattern),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,6 +22,25 @@ from app.services.memory.types import Episode, Fact, FactCreate, RetrievalResult
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_like_pattern(pattern: str) -> str:
|
||||||
|
"""
|
||||||
|
Escape SQL LIKE/ILIKE special characters to prevent pattern injection.
|
||||||
|
|
||||||
|
Characters escaped:
|
||||||
|
- % (matches zero or more characters)
|
||||||
|
- _ (matches exactly one character)
|
||||||
|
- \\ (escape character itself)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pattern: Raw search pattern from user input
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Escaped pattern safe for use in LIKE/ILIKE queries
|
||||||
|
"""
|
||||||
|
# Escape backslash first, then the wildcards
|
||||||
|
return pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||||
|
|
||||||
|
|
||||||
def _model_to_fact(model: FactModel) -> Fact:
|
def _model_to_fact(model: FactModel) -> Fact:
|
||||||
"""Convert SQLAlchemy model to Fact dataclass."""
|
"""Convert SQLAlchemy model to Fact dataclass."""
|
||||||
# SQLAlchemy Column types are inferred as Column[T] by mypy, but at runtime
|
# SQLAlchemy Column types are inferred as Column[T] by mypy, but at runtime
|
||||||
@@ -251,7 +270,9 @@ class SemanticMemory:
|
|||||||
if search_terms:
|
if search_terms:
|
||||||
conditions = []
|
conditions = []
|
||||||
for term in search_terms[:5]: # Limit to 5 terms
|
for term in search_terms[:5]: # Limit to 5 terms
|
||||||
term_pattern = f"%{term}%"
|
# Escape SQL wildcards to prevent pattern injection
|
||||||
|
escaped_term = _escape_like_pattern(term)
|
||||||
|
term_pattern = f"%{escaped_term}%"
|
||||||
conditions.append(
|
conditions.append(
|
||||||
or_(
|
or_(
|
||||||
FactModel.subject.ilike(term_pattern),
|
FactModel.subject.ilike(term_pattern),
|
||||||
@@ -295,12 +316,16 @@ class SemanticMemory:
|
|||||||
"""
|
"""
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
# Escape SQL wildcards to prevent pattern injection
|
||||||
|
escaped_entity = _escape_like_pattern(entity)
|
||||||
|
entity_pattern = f"%{escaped_entity}%"
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(FactModel)
|
select(FactModel)
|
||||||
.where(
|
.where(
|
||||||
or_(
|
or_(
|
||||||
FactModel.subject.ilike(f"%{entity}%"),
|
FactModel.subject.ilike(entity_pattern),
|
||||||
FactModel.object.ilike(f"%{entity}%"),
|
FactModel.object.ilike(entity_pattern),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(desc(FactModel.confidence), desc(FactModel.last_reinforced))
|
.order_by(desc(FactModel.confidence), desc(FactModel.last_reinforced))
|
||||||
|
|||||||
Reference in New Issue
Block a user