Nobody has started this yet — be first.
Business impact
Any user whose search term happens to contain a % or _ character (price tags like "50% off", filenames like "report_v2", usernames, version strings, etc.) gets search results that make no logical sense -- either far too many notes match (a % in the query silently turns into "match anything"), or a search returns a note that doesn't actually contain what was typed (a _ matches any single character). This is subtle enough that it usually isn't caught by manual testing, but once it ships, it's confusing and hard for an end user to describe precisely -- "search is broken" bug reports that are actually hard to reproduce without knowing to suspect LIKE wildcards specifically. Unlike the other bugs in this set, this one was not deliberately injected -- it was found already present in the untouched codebase during a real audit, which is exactly why it earns a higher priority than its documented "Medium" label: it is live, unstaged, and shipping right now.
Problem
list_notes builds a SQL LIKE pattern directly from user input: like = f"%{q}%". SQL's LIKE operator treats % as "any sequence of characters" and _ as "any single character" everywhere in the pattern, including inside the part that came from q itself. The code never escapes those two characters in the user-supplied search term before building the pattern.
Current behavior
A search for q=50% or q=file_v2 returns matches that make no logical sense -- either far too many notes match, or notes that don't actually contain the literal search term -- because the LIKE pattern is built directly from unescaped user input.
Expected behavior
A search for q=50% should match only notes containing the literal text 50%, not every note that merely contains 50 anywhere. A search for q=file_v2 should match only notes containing the literal text file_v2, not filexv2/fileAv2/etc. % and _ inside q should be treated as ordinary characters to search for, exactly like every other character.
Steps to reproduce
cd fastapi/notes_api
source .venv/bin/activate
uvicorn app.main:app --reload &
curl -s -X POST http://127.0.0.1:8000/notes -H "Content-Type: application/json"
-d '{"title":"Discount code: 50% off","content":"Discount code: 50% off"}'
curl -s -X POST http://127.0.0.1:8000/notes -H "Content-Type: application/json"
-d '{"title":"I saved exactly 50 dollars today","content":"I saved exactly 50 dollars today"}'
curl -s "http://127.0.0.1:8000/notes?q=50%25" # %25 is the URL-encoded literal "%"
Why this matters
This is a real, production-grade SQL LIKE metacharacter escaping bug -- the same category of mistake as forgetting to parameterize a query, except here the query is parameterized (SQLAlchemy handles that part correctly); the unescaped part is specifically the LIKE-pattern metacharacters within an otherwise-safe bound parameter. It requires recognizing that "safe from SQL injection" and "produces the search semantics you intended" are two different properties -- and it is precisely because this class of gap is easy to wave off as "not a real injection bug" that it tends to survive audits unfixed.
Suggested approach
Look at how like is constructed in list_notes, and at SQLAlchemy's .like() method signature -- it accepts an escape= keyword specifically for this. You'll need to escape occurrences of the escape character itself, plus % and _, in q before interpolating it into the pattern, and pass the matching escape= argument so the database actually treats your escape character as an escape rather than a literal character too.
Acceptance criteria
Verification
.venv/bin/pytest practicetickets/test_ticket06_like_wildcard_injection.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/like-wildcard-injectionFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/like-wildcard-injectionSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign in./data/notes.dbOr run it via Docker instead:
cd fastapi/notes_api
docker compose up --build # API on http://127.0.0.1:8000, /docs included
SQLite data is written inside the container to /app/data/notes.db, backed by the notes-data named volume declared in docker-compose.yml -- it survives docker compose restart and re-running docker compose up after down (without -v).
Work the tickets in practicetickets/ (ticket01 through ticket07); each names one pytest file in the same directory, separate from the project's own tests/ suite:
.venv/bin/pytest practicetickets/test_ticket01_tag_length_boundary.py -v # a single ticket
./practicetickets/run_tickets.sh # all 7, pass/fail summary
./practicetickets/run_tickets.sh -v # summary + full output
Note: ticket 02's bug also breaks two pre-existing tests in tests/test_notes.py (test_search_by_q_matches_title_and_content_case_insensitively and test_filter_by_tag_and_q_are_combinable) -- that's expected, and fixing ticket 02 should bring tests/ back to fully green. Run the main suite with .venv/bin/pytest tests/ -v.
Level 1
Fix a bug
Read existing behaviour, correct it.