Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions force-app/main/default/classes/UTIL_Typeahead.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<list<sObject>> results = Search.query(
'FIND \'' + sQuery + '*\'' + sInFields +
Expand Down
41 changes: 41 additions & 0 deletions force-app/main/default/classes/UTIL_Typeahead_TEST.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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<Id>());

Test.startTest();
list<sObject> 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.');
}
}
2 changes: 1 addition & 1 deletion force-app/main/default/pages/STG_PanelHome.page
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</div>
</div>
</div>
<div class="slds-grid">
<div class="slds-grid slds-wrap">
<div class="slds-col_padded slds-size_4-of-12">
<div class="slds-text-heading_medium slds-m-bottom_medium">
{!$Label.stgTools}
Expand Down
2 changes: 2 additions & 0 deletions force-app/main/default/pages/STG_PanelRelReciprocal.page
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
</div>
</div>
</apex:outputPanel>
<div class="slds-scrollable_x">
<table class="slds-table slds-table_bordered">
<thead>
<tr class="slds-text-heading_label">
Expand Down Expand Up @@ -125,5 +126,6 @@
</apex:repeat>
</tbody>
</table>
</div>
</apex:form>
</apex:page>
6 changes: 6 additions & 0 deletions force-app/main/default/pages/STG_SettingsManager.page
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
</style>

<script>
Expand Down
Loading