case 4716 - pick-list search text passed as SQL parameters

PickListSqlBuilder put the search text and the tag search text into the SQL
inside quotes with no escaping, reachable by any login (security review F18);
an apostrophe in a search also made the query fail. The builder now writes
@plq / @pltag and PickListFetcher adds them as Npgsql parameters (same %text%
match; wildcards typed by the user still apply).

Changelog: searching selection lists for text with an apostrophe now works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:28:43 -07:00
parent e58ab2c100
commit b1944b7ca3
5 changed files with 17 additions and 9 deletions

View File

@@ -182,7 +182,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s
| 4712 | Customer contact route allowlist + ownership checks | F13, F14, F15, F16, F17 | | 4712 | Customer contact route allowlist + ownership checks | F13, F14, F15, F16, F17 |
| 4713 | Contact users limited to customer roles | F28 | | 4713 | Contact users limited to customer roles | F28 |
| 4715 | Download file names checked (one helper) | F21. **Done** (`FileUtil.FullPathInFolderOrNull` in temp/backup exists checks and both log routes; test `DownloadNames`) | | 4715 | Download file names checked (one helper) | F21. **Done** (`FileUtil.FullPathInFolderOrNull` in temp/backup exists checks and both log routes; test `DownloadNames`) |
| 4716 | Pick-list search text as SQL parameters | F18 | | 4716 | Pick-list search text as SQL parameters | F18. **Done** (`@plq`/`@pltag` parameters; test `FetchUserPickListAutoCompleteApostrophe` fails on the old code, passes now; 9/9 pick-list tests) |
| 4717 | KPI interval allowlist | F26 | | 4717 | KPI interval allowlist | F26 |
| 4718 | Report and translation import need create rights | F19, F24. **Done** (contact and Service 403, BizAdmin 202; test `ImportRights`) | | 4718 | Report and translation import need create rights | F19, F24. **Done** (contact and Service 403, BizAdmin 202; test `ImportRights`) |
| 4719 | Report page network restricted; notification reports not run as SuperUser | F20 | | 4719 | Report page network restricted; notification reports not run as SuperUser | F20 |

View File

@@ -195,7 +195,7 @@ take the next free number; a fixed or accepted finding keeps its ID and changes
| F1 | Critical (installs that kept it) | **fixed in case 4711** | Placeholder JWT secret in the LAN/IIS and Linux server config templates | | F1 | Critical (installs that kept it) | **fixed in case 4711** | Placeholder JWT secret in the LAN/IIS and Linux server config templates |
| F19 | Critical | **fixed in case 4718** | Any logged-in user, customer contacts included, can import report templates, whose JavaScript runs in the server's browser | | F19 | Critical | **fixed in case 4718** | Any logged-in user, customer contacts included, can import report templates, whose JavaScript runs in the server's browser |
| F21 | Critical | **fixed in case 4715** | Download file names from the URL are not checked: `report/download` and `export/download` may serve files outside the temp folder on Windows | | F21 | Critical | **fixed in case 4715** | Download file names from the URL are not checked: `report/download` and `export/download` may serve files outside the temp folder on Windows |
| F18 | Critical | confirmed (code) | Pick-list search text is put into SQL without escaping; any logged-in user can reach it | | F18 | Critical | **fixed in case 4716** | Pick-list search text is put into SQL without escaping; any logged-in user can reach it |
| F26 | Critical | confirmed (code) | KPI `interval` criterion is put into SQL unchecked; reachable by staff users down to TechRestricted | | F26 | Critical | confirmed (code) | KPI `interval` criterion is put into SQL unchecked; reachable by staff users down to TechRestricted |
| F13 | High | **confirmed** | Customer contacts can open any customer's work order (customer view, customer report, attachments) by id | | F13 | High | **confirmed** | Customer contacts can open any customer's work order (customer view, customer report, attachments) by id |
| F14 | High | **confirmed** | Customer contacts can read, and by code change, delete and create, any customer's service requests | | F14 | High | **confirmed** | Customer contacts can read, and by code change, delete and create, any customer's service requests |

View File

@@ -34,6 +34,7 @@ IN PROGRESS
**Fixed** **Fixed**
- Server: searching selection lists for text that contains an apostrophe (for example a name like O'Brien) now works.
- Server: hardened data table filter handling of number and role filter values. - Server: hardened data table filter handling of number and role filter values.
- Server: data table date filters for "greater than or equal", "less than" and "not equal" could include or exclude records on the boundary of the selected date. - Server: data table date filters for "greater than or equal", "less than" and "not equal" could include or exclude records on the boundary of the selected date.
- Server: the database connection used for administrative operations is now built correctly when the database name also appears elsewhere in the connection setting. - Server: the database connection used for administrative operations is now built correctly when the database name also appears elsewhere in the connection setting.

View File

@@ -56,6 +56,11 @@ namespace AyaNova.PickList
//GET DATA RETURN ROWS //GET DATA RETURN ROWS
command.CommandText = q; command.CommandText = q;
//case 4716: search texts go in as parameters (same %text% match as before, wildcards typed by the user still apply)
if (q.Contains("@plq"))
command.Parameters.Add(new Npgsql.NpgsqlParameter("plq", $"%{autoCompleteQuery}%"));
if (q.Contains("@pltag"))
command.Parameters.Add(new Npgsql.NpgsqlParameter("pltag", $"%{tagSpecificQuery}%"));
try try
{ {
using (var dr = await command.ExecuteReaderAsync()) using (var dr = await command.ExecuteReaderAsync())

View File

@@ -104,13 +104,15 @@ namespace AyaNova.PickList
} }
} }
//case 4716: the search texts are SQL parameters (@plq, @pltag), added by PickListFetcher, never part of the SQL text
//PROCESS TAG SPECIFIC QUERY //PROCESS TAG SPECIFIC QUERY
// //
if (HasTagSpecificQuery) if (HasTagSpecificQuery)
{ {
//get the tag column //get the tag column
AyaPickListFieldDefinition tagColumn = pickList.ColumnDefinitions.FirstOrDefault(z => z.ColumnDataType == UiFieldDataType.Tags); AyaPickListFieldDefinition tagColumn = pickList.ColumnDefinitions.FirstOrDefault(z => z.ColumnDataType == UiFieldDataType.Tags);
TagSpecificWhereFragment = $"(array_to_string({tagColumn.GetSqlValueColumnName()},',') like '%{tagSpecificQuery}%')"; TagSpecificWhereFragment = $"(array_to_string({tagColumn.GetSqlValueColumnName()},',') like @pltag)";
} }
@@ -150,9 +152,9 @@ namespace AyaNova.PickList
if (HasAutoCompleteQuery && !HasTagSpecificQuery) if (HasAutoCompleteQuery && !HasTagSpecificQuery)
{ {
if (ServerGlobalBizSettings.Cache.FilterCaseSensitive) if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
sWhere = $"(array_to_string({valueColumnName},',') like '%{autoCompleteQuery}%')"; sWhere = $"(array_to_string({valueColumnName},',') like @plq)";
else else
sWhere = $"(lower(array_to_string({valueColumnName},',')) like lower('%{autoCompleteQuery}%'))"; sWhere = $"(lower(array_to_string({valueColumnName},',')) like lower(@plq))";
} }
@@ -165,9 +167,9 @@ namespace AyaNova.PickList
lOrderBy.Add(valueColumnName); lOrderBy.Add(valueColumnName);
if (HasAutoCompleteQuery) if (HasAutoCompleteQuery)
if (ServerGlobalBizSettings.Cache.FilterCaseSensitive) if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
sWhere = $"({valueColumnName} like '%{autoCompleteQuery}%')"; sWhere = $"({valueColumnName} like @plq)";
else else
sWhere = $"(lower({valueColumnName}) like lower('%{autoCompleteQuery}%'))"; sWhere = $"(lower({valueColumnName}) like lower(@plq))";
} }
else else
@@ -187,9 +189,9 @@ namespace AyaNova.PickList
//(cast (aworkorder.serial as text) like '%some%') //(cast (aworkorder.serial as text) like '%some%')
if (HasAutoCompleteQuery) if (HasAutoCompleteQuery)
if (ServerGlobalBizSettings.Cache.FilterCaseSensitive) if (ServerGlobalBizSettings.Cache.FilterCaseSensitive)
sWhere = $"(cast ({valueColumnName} as text) like '%{autoCompleteQuery}%')"; sWhere = $"(cast ({valueColumnName} as text) like @plq)";
else else
sWhere = $"(lower(cast ({valueColumnName} as text)) like lower('%{autoCompleteQuery}%'))"; sWhere = $"(lower(cast ({valueColumnName} as text)) like lower(@plq))";
} }