Let a Search Value Stay a Value
A search box should select an asset, not rewrite the query or change whose records are visible. Keep values, query structure and permissions at separate boundaries.
14 sections · 4 questions · 2 coding challenges
The arrow keys move between sections too.
Separating Query Structure from Supplied Data
BOTIE
Database access control developed alongside shared data systems because syntactically valid queries can still access the wrong records. Parameter binding and authorisation solve different problems. Botie’s maintenance search needs both, even though our isolated browser database has no production users or server roles.
Sources for this history
An Asset Search Should Not Become a New SQL Command
A technician types an asset name into a search box. The application pastes it into a SQL string. A quote in that name can change how the database reads the statement instead of remaining ordinary text.
We will bind values through the database driver, keep the tenant scope supplied by trusted application state, and expose only the fields the interface needs. The SQLite exercise demonstrates a scoped view; a deployed service also needs real database and application permissions.
Invented maintenance records. SQL programs run in an isolated SQLite database; the workshop illustrates the operation with records rather than parsing SQL.
Step 1 of 1
Keep the source of each input visible
Two values enter the query from different trust boundaries.
- 1
Authenticated request
Trusted tenant: north - 2
Search box
Supplied asset: P7 - 3
Query structure
Fixed tenant and asset predicates - 4
Driver binding
Both values remain data - 5
Result projection
Only fields the interface needs
Parameter binding does not grant permission
The search is evaluated inside an already scoped set, and exact values never become executable query text.
Does the selected asset belong to the trusted tenant?
Filter the record set by trusted tenant scope before matching the supplied asset value.
BOTIE
Visible job IDs
- 0J1
Follow the operations
1. Job records
[ { "id": "J1", "tenant": "north", "asset": "P7" }, { "id": "J2", "tenant": "south", "asset": "P7" } ]2. Trusted tenant selector
{ "tenant": "north" }3. Match every named selector field
[ { "id": "J1", "tenant": "north", "asset": "P7" } ]4. Supplied asset value
{ "asset": "P7" }5. Match every named selector field
[ { "id": "J1", "tenant": "north", "asset": "P7" } ]6. Read the resulting record IDs
- 0J1
7. Result
- 0J1
The widget has no SQL parser and no real login. The Python challenge exercises parameter binding; production permissions need a deployed service design.
1. Keep a query’s shape separate from its values
The query structure says which table and fields are involved and how rows are filtered. The asset name is a value placed into that fixed structure. Driver placeholders preserve that distinction.
Manually adding quotation marks or escaping a few characters is easy to get wrong across dialects and encodings. Use the driver’s binding API. A parameter containing a quote or SQL-looking text is then compared as a value rather than becoming a new predicate.
The exercise uses a local database and harmless fixture strings. It does not connect to any external service.
2. Trusted scope must come from the application
Binding tenant and asset values prevents them from changing SQL syntax. It does not prove that the caller may choose any tenant. Obtain the tenant from authenticated server-side context and enforce access again at the data boundary appropriate to the service.
Do not place database credentials in a browser worksheet or give a model unrestricted SQL access because its outputs pass a schema check. A model-generated query is still code whose available operations and data access need deliberate limits.
The workshop shows exact record matching. It does not interpret strings as SQL commands.
Check yourself
Questions on Parts 1 to 2
Question 1 of 2
What is still missing?
Use the information below.
The tenant value is safely bound as a parameter, but the browser chooses any tenant it likes.3. Bind values through Python sqlite3
Python sqlite3 accepts placeholders such as ? and a separate parameter sequence. A single parameter still needs a one-item sequence. fetchall returns a list of row tuples in the requested order.
The programming exercise writes find_jobs(connection, tenant, asset). Its tenant argument represents scope already established by the caller; the function does not implement login or authorisation. The SQL string remains fixed while the parameters change.
Placeholder spelling varies between database drivers. Follow the driver used by the application.
A quoted identifier remains ordinary data
This complete local program searches for a value containing a quote.
import sqlite3
connection = sqlite3.connect(":memory:")
connection.execute("CREATE TABLE assets(id TEXT)")
connection.execute("INSERT INTO assets VALUES(?)", ("P'7",))
print(connection.execute("SELECT id FROM assets WHERE id = ?", ("P'7",)).fetchall())[("P'7",)]The quote belongs to the stored value and does not alter the SQL statement.
4. Identifiers need a different choice mechanism
A value placeholder does not generally substitute a table name, column name or sort-direction keyword. If the interface offers a sort order, map its allowed choices to known query fragments or use the driver’s supported identifier-composition API.
Do not convert an arbitrary submitted string into a column identifier and assume value binding elsewhere protects it. Keep the list of accessible fields narrow. The safe design depends on which parts are data and which parts would change executable structure.
An allowlist chooses among known structural options. It is not an attempt to sanitise arbitrary SQL.
5. Expose a smaller read surface
Our SQL exercise creates a view containing only north’s open jobs and the fields used by the queue. It demonstrates projection and a stable predicate in one named relation. Inspecting the view makes accidental private_note inclusion easy to notice.
SQLite does not provide PostgreSQL-style server users and GRANT privileges. A local caller with direct access to this database can still read the underlying table. In a server database, appropriate roles and grants can restrict access to the view or approved operations; application checks and, where suitable, row-level security add separate controls.
A view is not an authentication system.
Return only the fields needed for the queue
This fixed projection leaves internal notes out of the result.
| id | tenant | asset_id | status | private_note |
|---|---|---|---|---|
| J1 | north | P7 | open | private A |
| J2 | south | P8 | open | private B |
| J3 | north | P9 | done | private C |
Database setup for this example
CREATE TABLE jobs(id TEXT PRIMARY KEY NOT NULL,tenant TEXT NOT NULL,asset_id TEXT NOT NULL,status TEXT NOT NULL,private_note TEXT);
INSERT INTO jobs VALUES('J1','north','P7','open','private A'),('J2','south','P8','open','private B'),('J3','north','P9','done','private C');SELECT id,asset_id,status FROM jobs WHERE tenant='north' AND status='open' ORDER BY id;| id | asset_id | status |
|---|---|---|
| J1 | P7 | open |
The result includes the permitted row and omits private_note. The exercise turns that relation into a named view.
6. Give the service only the operations it needs
Use separate credentials or roles for migrations, application writes and read-only reporting where the architecture supports them. A report process rarely needs permission to change schemas. Revoke unnecessary inherited or public grants and inspect effective privileges rather than only the most recent GRANT statement.
PostgreSQL row-level security can constrain visible rows under defined policies, with important ownership and privileged-role exceptions. Treat it as a configured database feature to test, not a promise made by a WHERE clause in one code path.
The browser examples do not configure production roles or claim to demonstrate complete tenant isolation.
7. Bound the query even when its values are safe
A parameterised query can still be expensive or return too much permitted data. Apply pagination, response-size limits and bounded execution appropriate to the service. Avoid logging secrets or unnecessary row content while retaining enough identifiers to investigate access decisions.
Test a literal quote, an SQL-looking asset name, an absent asset, and the same asset ID in another tenant. Those cases check binding and scoping separately. Operational testing must additionally exercise the actual deployed role and every exposed endpoint.
Correct syntax handling is one necessary boundary in a larger access design.
Check yourself
Questions on Parts 1 to 7
Question 1 of 2
How should the server choose that structural SQL fragment?
Use the information below.
The UI offers sort by created time or by asset ID.Try it with Botie
Build with Botie
Write and run Python