If you can, I would go ahead and convert Categories to Entries, following the Entrification plan. For Categories fields, the hierarchy is always maintained, so you can't select child categories without selecting all their ancestors. But for Entries fields, this is optional, so you can just turn off the option to maintain the hierarchy. This will allow your editors to select child categories without selecting their parents.
If you can't do that, you can also filter the selected categories to only include leaf nodes (nodes without children):
{% set selectedLeafCategories = entry.categoriesField.leaves() %}
{% set products = craft.entries()
.section('products')
.relatedTo(selectedLeafCategories)
.all()
%}
However, this will only find actual leaf nodes, so only categories that have no children at all. If the editor only selects a parent category without selecting any children, those still won't be included in the results above, because they aren't leaf nodes.
I don't think it's possible to apply this logic in the query itself. But you can use a Twig filter to remove any parent categories where at least one of their child categories is selected as well:
{% set selectedCategories = entry.categoriesField.all() %}
{% set leafCategories = selectedCategories|filter(
category => selectedCategories|filter(current => category.isAncestorOf(current)) is empty
)|values %}
Keep in mind that this might make the behaviour a bit difficult to understand for your editors. The entrification is definitely the better solution, if that is possible for your project at this time.