Fix SQLite LIKE wildcard escaping in IP filter queries

- Add escape_like() helper to escape % and _ wildcards in LIKE queries
- Update fail2ban_db_repo.get_history_page() to use escaping
- Update history_archive_repo.get_archived_history() to use escaping
- Add ESCAPE clause to all LIKE queries
- Add comprehensive unit tests for escape_like function
- Add integration tests for LIKE wildcard handling
- Document LIKE escaping best practices in Backend-Development.md

Fixes TASK-017: Prevent unintended LIKE matches when IP filter contains
special characters like underscore or percent sign.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 14:07:49 +02:00
parent 94bdabe622
commit 667ab674ca
7 changed files with 234 additions and 4 deletions

View File

@@ -311,6 +311,59 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
---
## 6.1 Database Query Conventions
### LIKE Queries and Wildcard Escaping
SQLite's `LIKE` operator treats `%` (any sequence of characters) and `_` (any single character) as wildcards. When querying with user-supplied filters that may contain these characters, you must escape them to prevent unintended matches.
**The Problem:**
```python
# Bad — ip_filter="10.0.0_" matches "10.0.0.1", "10.0.0.2", etc.
ip_filter = "10.0.0_"
await db.execute(
"SELECT * FROM bans WHERE ip LIKE ?",
(f"{ip_filter}%",) # ← wildcard characters not escaped
)
```
**The Solution:**
Use the `escape_like()` helper from `app.utils.fail2ban_db_utils`:
```python
from app.utils.fail2ban_db_utils import escape_like
# Good — wildcard characters are escaped
ip_filter = "10.0.0_"
await db.execute(
"SELECT * FROM bans WHERE ip LIKE ? ESCAPE '\\'",
(f"{escape_like(ip_filter)}%",) # ← underscores escaped to literal
)
```
**How `escape_like()` works:**
The function escapes backslashes first, then `%` and `_` signs:
```python
def escape_like(s: str) -> str:
return s.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
```
**Key rules:**
1. **Backslash escapes first** — to prevent double-escaping when the input contains backslashes.
2. **Add `ESCAPE '\\'` to the SQL** — tells SQLite which character to use for escaping.
3. **Dots are not wildcards** — they do not need escaping; normal IP addresses pass through unchanged.
**Test example:**
```python
assert escape_like("10.0.0_") == "10.0.0\\_"
assert escape_like("10.0.0%test") == "10.0.0\\%test"
assert escape_like("10.0.0.1") == "10.0.0.1" # Unchanged
```
---
## 7. Logging
- Use **structlog** for every log message.