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, $activeTypes,
$this->associationEntityTypes() $this->associationEntityTypes()
)); ));
foreach ($customerNumbers as $customerNumber) { $associationTypes = array_values(array_diff($associationTypes, $ownOnlyTypes));
$associated = $this->executeLexicalSearch( if (!empty($associationTypes)) {
$associationTypes, foreach ($customerNumbers as $customerNumber) {
[(string)$customerNumber], $associated = $this->executeLexicalSearch(
[], $associationTypes,
$ownOnlyTypes, [(string)$customerNumber],
$ownCustomerNumber, [],
$permissionsCatalogAll, $ownOnlyTypes,
$permissionsCatalogOwn, $ownCustomerNumber,
$moduleConfigVisibility, $permissionsCatalogAll,
[$customerNumber] $permissionsCatalogOwn,
); $moduleConfigVisibility,
foreach ($associated as &$item) { [$customerNumber]
if (!isset($item['association_reason'])) { );
$item['association_reason'] = 'customer:' . $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); 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 { it('expands danish discount wording into lexical discount synonyms', function (): void {
$parser = new FakeSystemSearchIntentParser(); $parser = new FakeSystemSearchIntentParser();
$service = new TestableSystemSearchService($parser, [[ $service = new TestableSystemSearchService($parser, [[