The Delphi Search Box Problem: Parameterizing FireDAC Queries by Default
Concatenating user input into SQL strings is a liability. See how to use TFDQuery parameters in Delphi to separate code from data, reduce injection risk, and avoid type-mapping surprises.
28 Aug 2025, 19:10 UTC

The search box that becomes an injection point
You add a lookup box to a Delphi VCL form. The user types a city name, and the app filters an orders grid. The quickest path is to build the SQL string by concatenation:
FDQuery.SQL.Text := 'SELECT * FROM Orders WHERE City = ''' + Edit1.Text + '''';
It works in testing. Then a user types London' OR '1'='1 and the grid suddenly shows every order in the database. Or a support technician pastes a value with an apostrophe—O'Brien—and the query fails with a syntax error. The root cause is that concatenation merges your program's instructions with the user's data into a single string the database must parse.
The useful takeaway: in Delphi applications using FireDAC, parameterized queries should be the default for any value that originates outside your code. They enforce a boundary between SQL structure and runtime data.
What parameterized queries actually separate
SQL injection happens because the database engine cannot distinguish between the SELECT statement you wrote and the text a user typed when both arrive as one contiguous string. A parameterized query sends the two parts separately. First, the driver submits a SQL template with placeholders—WHERE City = :City. Second, it binds the actual value to that placeholder. The engine compiles the template once and treats the bound value strictly as data, not as executable syntax.
FireDAC exposes this through the TFDQuery.Params collection. Named parameters use a colon prefix, and you assign values with typed properties such as AsString, AsInteger, or AsFloat. Because the template is stable, the database can cache the execution plan—the precompiled strategy for finding rows—and reuse it across calls. That reduces parsing overhead on busy forms and short-lived service requests.
A concrete TFDQuery example
Assume a VCL Forms Application in the Delphi IDE with a TFDConnection named FDConnection1 and a TFDQuery named FDQueryOrders dropped on a form. The connection is already configured for your target database. The following procedure runs from a button click handler and filters by city and minimum order total.
procedure TOrderForm.FilterOrders(const ACity: string; AMinTotal: Double);
begin
FDQueryOrders.Close;
FDQueryOrders.SQL.Text :=
'SELECT OrderID, OrderDate, Total ' +
'FROM Orders ' +
'WHERE City = :City AND Total >= :MinTotal ' +
'ORDER BY OrderDate DESC';
FDQueryOrders.ParamByName('City').AsString := ACity;
FDQueryOrders.ParamByName('MinTotal').AsFloat := AMinTotal;
FDQueryOrders.Open;
end;
Where to run it: inside a VCL form unit in the Delphi IDE, with the FireDAC components already linked to a valid database connection. No special OS permissions are required beyond the normal database credentials used by TFDConnection.
Placeholders: :City and :MinTotal correspond exactly to the ParamByName calls. FireDAC parameter names are case-insensitive, but a typo raises an exception at runtime.
Expected check: enter O'Brien into the city field. A concatenated query would break with a quoting error. With parameters, the value is bound safely and the query returns only rows where the city literally matches O'Brien. Entering London' OR '1'='1 returns zero rows rather than the entire table.
Relevant risk: if you reuse the same TFDQuery instance and forget to assign a parameter, it may retain the value from the previous execution. Always set every parameter explicitly before calling Open.
Where parameters bite back: type mapping and dynamic identifiers
Parameters are not free. FireDAC maps each parameter to a database type, and mismatches can cause silent truncation or conversion errors. If a column is INTEGER and you assign ParamByName('CustomerID').AsString := Edit1.Text, some drivers will coerce the string, while others may reject it or truncate unexpectedly. Prefer the typed property that matches the column—AsInteger for numeric IDs, AsDateTime for dates—and validate or convert user input before assignment.
A second limitation is that standard SQL does not allow parameters to replace table names, column names, or ORDER BY expressions. If your application builds dynamic sorting or switches tables at runtime, you must assemble that part of the SQL text yourself. The safe pattern is to validate the identifier against a hard-coded whitelist rather than passing raw user text into the statement.
FireDAC itself has been part of Delphi since the XE5 era, though the exact API surface and pooling behavior have evolved in later releases. Component availability also depends on your Delphi edition. Before committing to this pattern, confirm that TFDConnection, TFDQuery, and TFDManager appear in the Tool Palette and that your uses clause can reference the FireDAC.Comp.Client unit.
Make parameterization the default
Treat any string that crosses a user-input boundary as hostile until proven otherwise. A simple rule for your codebase: TFDQuery.SQL.Text should never contain a variable that holds user data. Values go through ParamByName; identifiers go through a whitelist.
To verify the behavior in your own project, enable TFDMonitor or review your database's query log. You should see the placeholder template submitted once and the bound values arriving separately. If you still see fully assembled SQL strings with user text embedded, the concatenation path is still live.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.