Summary
When multiple entries are configured in allowExceptInFunctions (or its aliases disallowInFunctions, allowExceptInMethods, disallowInMethods), only the first entry actually disallows. Any subsequent entries are silently ignored.
Root cause
The loop in Allowed::isAllowed() returns true on the first entry that does not match the current call, instead of continuing through all entries:
foreach ($disallowed->getAllowExceptInCalls() as $call) {
if (!$this->callMatches($scope, $call)) {
return true; // bails out on first non-match
}
}
With a single entry this works correctly. With [A, B] and the current call being A: the loop reaches B, B does not match, and returns true (allowed) — even though A is in the disallow list.
Expected behavior
Same as allowExceptIn for paths — iterate all entries, return false if any entry matches the current call, return true only after all entries have been checked without a match:
if ($disallowed->getAllowExceptInCalls()) {
foreach ($disallowed->getAllowExceptInCalls() as $call) {
if ($this->callMatches($scope, $call)) {
return false;
}
}
return true;
}
Reproduction
parameters:
disallowedFunctionCalls:
-
function: 'foo()'
disallowInFunctions:
- 'Bar::methodA'
- 'Bar::methodB'
Calling foo() inside Bar::methodA is not reported as forbidden, because the loop reaches Bar::methodB, which does not match, and returns allowed.
Affected config keys
allowExceptInFunctions / disallowInFunctions
allowExceptInMethods / disallowInMethods
Summary
When multiple entries are configured in
allowExceptInFunctions(or its aliasesdisallowInFunctions,allowExceptInMethods,disallowInMethods), only the first entry actually disallows. Any subsequent entries are silently ignored.Root cause
The loop in
Allowed::isAllowed()returnstrueon the first entry that does not match the current call, instead of continuing through all entries:With a single entry this works correctly. With
[A, B]and the current call beingA: the loop reachesB,Bdoes not match, and returnstrue(allowed) — even thoughAis in the disallow list.Expected behavior
Same as
allowExceptInfor paths — iterate all entries, returnfalseif any entry matches the current call, returntrueonly after all entries have been checked without a match:Reproduction
Calling
foo()insideBar::methodAis not reported as forbidden, because the loop reachesBar::methodB, which does not match, and returns allowed.Affected config keys
allowExceptInFunctions/disallowInFunctionsallowExceptInMethods/disallowInMethods