From fc0dc7c633bba245d609d9a7dcd40ed1c7177c03 Mon Sep 17 00:00:00 2001 From: Luke Parrott Date: Tue, 15 Sep 2026 11:14:25 -0500 Subject: [PATCH 1/2] Fix for security bug --- .../main/default/classes/UTIL_Typeahead.cls | 41 +++++++++++++++++-- .../default/classes/UTIL_Typeahead_TEST.cls | 41 +++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/force-app/main/default/classes/UTIL_Typeahead.cls b/force-app/main/default/classes/UTIL_Typeahead.cls index e98f893b59c..4123b714a57 100644 --- a/force-app/main/default/classes/UTIL_Typeahead.cls +++ b/force-app/main/default/classes/UTIL_Typeahead.cls @@ -35,6 +35,36 @@ */ public with sharing class UTIL_Typeahead { + /******************************************************************************************************* + * @description Allow-list pattern for filterClause: one or more "field operator literal" comparisons + * joined by AND/OR. Quoted literals cannot contain a single quote, so a caller cannot break out of the + * literal to inject additional SOSL/SOQL syntax. + ********************************************************************************************************/ + private static final Pattern FILTER_CLAUSE_PATTERN = Pattern.compile( + '(?i)^\\s*[a-z_][a-z0-9_]*\\s*(=|!=|<>|<=|>=|<|>|like)\\s*(\'[^\']*\'|-?\\d+(\\.\\d+)?|true|false|null)' + + '(\\s+(and|or)\\s+[a-z_][a-z0-9_]*\\s*(=|!=|<>|<=|>=|<|>|like)\\s*(\'[^\']*\'|-?\\d+(\\.\\d+)?|true|false|null))*\\s*$' + ); + + private static final Pattern FILTER_CLAUSE_SPLIT = Pattern.compile('(?i)\\s+(?:and|or)\\s+'); + private static final Pattern FILTER_FIELD_TOKEN = Pattern.compile('(?i)^\\s*([a-z_][a-z0-9_]*)\\s*(?:=|!=|<>|<=|>=|<|>|like)'); + + public class InvalidFilterClauseException extends Exception {} + + /******************************************************************************************************* + * @description Confirms every field referenced in an already syntax-validated filterClause actually + * exists on sObjectName, using the same describe-driven validation UTIL_Where/UTIL_Describe use + * elsewhere for dynamic SOQL. Guards against field names that are syntactically valid but bogus. + ********************************************************************************************************/ + private static Boolean allFilterClauseFieldsAreValid(String filterClause, String sObjectName) { + for (String clause : FILTER_CLAUSE_SPLIT.split(filterClause)) { + Matcher fieldMatcher = FILTER_FIELD_TOKEN.matcher(clause); + if (!fieldMatcher.find() || !UTIL_Describe.isValidField(sObjectName, fieldMatcher.group(1))) { + return false; + } + } + return true; + } + /******************************************************************************************************* * @description Searches the database (using SOSL) for the records that meet the criteria. * @param queryString (SearchQuery) The word of phrase to search for. @@ -69,9 +99,14 @@ public with sharing class UTIL_Typeahead { String sLimit = (recordLimit == null || recordLimit == 0 || recordLimit >= 2000) ? '' : ( ' LIMIT ' + String.valueOf(recordLimit)); - // can't escape the filter clause - String sWhere = (filterClause == null || filterClause == '') ? '' : - ( ' WHERE ' + filterClause ); + String sWhere = ''; + if (filterClause != null && filterClause != '') { + if (!FILTER_CLAUSE_PATTERN.matcher(filterClause).matches() || + !allFilterClauseFieldsAreValid(filterClause, sObjectName)) { + throw new InvalidFilterClauseException('Invalid filterClause.'); + } + sWhere = ' WHERE ' + filterClause; + } list> results = Search.query( 'FIND \'' + sQuery + '*\'' + sInFields + diff --git a/force-app/main/default/classes/UTIL_Typeahead_TEST.cls b/force-app/main/default/classes/UTIL_Typeahead_TEST.cls index 6bdd39311f4..e4a1ab4374e 100644 --- a/force-app/main/default/classes/UTIL_Typeahead_TEST.cls +++ b/force-app/main/default/classes/UTIL_Typeahead_TEST.cls @@ -56,4 +56,45 @@ public with sharing class UTIL_Typeahead_TEST { system.assertEquals('Test Account', accts[0].get('Name')); system.assertEquals('Another Account', accts[1].get('Name')); } + + private static testMethod void testValidFilterClauseIsAccepted() { + Test.setFixedSearchResults(new list()); + + Test.startTest(); + list accts = UTIL_Typeahead.searchRecords( + 'test', 'Account', null, null, 'Name = \'Test Account\' AND BillingCity = \'Test City\'', 'Id', null + ); + Test.stopTest(); + + system.assertEquals(0, accts.size()); + } + + private static testMethod void testNonexistentFieldInFilterClauseIsRejected() { + Boolean threwException = false; + + try { + UTIL_Typeahead.searchRecords( + 'test', 'Account', null, null, 'NotARealField__c = \'x\'', 'Id', null + ); + } catch (UTIL_Typeahead.InvalidFilterClauseException e) { + threwException = true; + } + + system.assertEquals(true, threwException, 'A filterClause referencing a nonexistent field should be rejected.'); + } + + private static testMethod void testInjectedFilterClauseIsRejected() { + Boolean threwException = false; + + try { + UTIL_Typeahead.searchRecords( + 'test', 'Account', null, null, + 'Name = \'\' OR Name LIKE \'%\') RETURNING Contact(Id) --', 'Id', null + ); + } catch (UTIL_Typeahead.InvalidFilterClauseException e) { + threwException = true; + } + + system.assertEquals(true, threwException, 'An unsafe filterClause should be rejected.'); + } } \ No newline at end of file From b4f91204270a1e6731d7d512477a4ef635b923ac Mon Sep 17 00:00:00 2001 From: Luke Parrott Date: Mon, 5 Oct 2026 16:50:51 -0500 Subject: [PATCH 2/2] Fix for scrolling in Settings page when zoomed --- force-app/main/default/pages/STG_PanelHome.page | 2 +- force-app/main/default/pages/STG_PanelRelReciprocal.page | 2 ++ force-app/main/default/pages/STG_SettingsManager.page | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/force-app/main/default/pages/STG_PanelHome.page b/force-app/main/default/pages/STG_PanelHome.page index 82b92c51259..02da7058017 100644 --- a/force-app/main/default/pages/STG_PanelHome.page +++ b/force-app/main/default/pages/STG_PanelHome.page @@ -13,7 +13,7 @@ -
+
{!$Label.stgTools} diff --git a/force-app/main/default/pages/STG_PanelRelReciprocal.page b/force-app/main/default/pages/STG_PanelRelReciprocal.page index 1a6009f4693..24b61eb59a2 100644 --- a/force-app/main/default/pages/STG_PanelRelReciprocal.page +++ b/force-app/main/default/pages/STG_PanelRelReciprocal.page @@ -79,6 +79,7 @@
+
@@ -125,5 +126,6 @@
+
\ No newline at end of file diff --git a/force-app/main/default/pages/STG_SettingsManager.page b/force-app/main/default/pages/STG_SettingsManager.page index bae297de168..ac56ea94a1d 100644 --- a/force-app/main/default/pages/STG_SettingsManager.page +++ b/force-app/main/default/pages/STG_SettingsManager.page @@ -44,6 +44,12 @@ .hide { display: none; } + + /* Contain horizontal overflow of wide panels to the content column so the sidebar and headings don't scroll with it (WCAG Reflow) */ + .slds .content { + min-width: 0; + overflow-x: auto; + }