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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user