Merge pull request #221 from copenhagentruckwash/fix-system-search-associations-vulnerability

Prevent association expansion from bypassing own-only access
This commit is contained in:
Jeppe B
2026-06-01 23:31:00 +02:00
committed by GitHub
2 changed files with 44 additions and 17 deletions
@@ -207,25 +207,28 @@ class system_search_service
$activeTypes,
$this->associationEntityTypes()
));
foreach ($customerNumbers as $customerNumber) {
$associated = $this->executeLexicalSearch(
$associationTypes,
[(string)$customerNumber],
[],
$ownOnlyTypes,
$ownCustomerNumber,
$permissionsCatalogAll,
$permissionsCatalogOwn,
$moduleConfigVisibility,
[$customerNumber]
);
foreach ($associated as &$item) {
if (!isset($item['association_reason'])) {
$item['association_reason'] = 'customer:' . $customerNumber;
$associationTypes = array_values(array_diff($associationTypes, $ownOnlyTypes));
if (!empty($associationTypes)) {
foreach ($customerNumbers as $customerNumber) {
$associated = $this->executeLexicalSearch(
$associationTypes,
[(string)$customerNumber],
[],
$ownOnlyTypes,
$ownCustomerNumber,
$permissionsCatalogAll,
$permissionsCatalogOwn,
$moduleConfigVisibility,
[$customerNumber]
);
foreach ($associated as &$item) {
if (!isset($item['association_reason'])) {
$item['association_reason'] = 'customer:' . $customerNumber;
}
$item['score'] = max((int)$item['score'], 35);
}
$item['score'] = max((int)$item['score'], 35);
$initialResults = $this->mergeResults($initialResults, $associated);
}
$initialResults = $this->mergeResults($initialResults, $associated);
}
}
}
@@ -385,6 +385,30 @@ it('caps AI-driven expanded terms to prevent query amplification', function ():
expect($maxLen)->toBeLessThanOrEqual(64);
});
it('does not expand associations for own-only entity types', function (): void {
$parser = new FakeSystemSearchIntentParser();
$service = new TestableSystemSearchService($parser, [
[[
'entity_type' => 'customers',
'entity_id' => '10',
'title' => 'Acme',
'customer_number' => 1234,
'score' => 80,
]],
[],
]);
$service->search([
'query' => 'acme',
'allowed_types' => ['customers', 'orders'],
'own_only_types' => ['orders'],
'own_customer_number' => 4444,
'include_associations' => true,
]);
expect(count($service->lexicalCalls))->toBe(1);
});
it('expands danish discount wording into lexical discount synonyms', function (): void {
$parser = new FakeSystemSearchIntentParser();
$service = new TestableSystemSearchService($parser, [[