Skip to content
Merged
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
54 changes: 37 additions & 17 deletions src/Auth/AclTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,41 @@ protected function _checkUser(ArrayAccess|array $user, array $params): bool {
*/
protected function _isProtectedPrefix($prefix, array $protectedPrefixes) {
foreach ($protectedPrefixes as $protectedPrefix) {
if ($prefix === $protectedPrefix || strpos($prefix, $protectedPrefix . '/') === 0) {
if ($prefix === $protectedPrefix || str_starts_with((string)$prefix, $protectedPrefix . '/')) {
return true;
}
}

return false;
}

/**
* Compare a request-side route slot value (plugin/prefix) against a rule-side value.
*
* Both `null` and the empty string are treated as "no plugin / no prefix". Anything
* else must match exactly. The previous code used `!empty()` which coerced `'0'` and
* other PHP-falsy values to "no plugin", which is too lossy for an authorization
* matcher. Using an explicit null/empty-string check is more honest about the input
* space and easier to audit.
*
* @param mixed $request The value from the current request's routing params.
* @param mixed $rule The value declared on the ACL rule.
* @return bool True when both sides describe the same slot.
*/
protected function _matchesRouteSlot($request, $rule): bool {
$requestEmpty = $request === null || $request === '';
$ruleEmpty = $rule === null || $rule === '';

if ($requestEmpty && $ruleEmpty) {
return true;
}
if ($requestEmpty xor $ruleEmpty) {
return false;
}

return $request === $rule;
}

/**
* @param array<string, int|string> $userRoles
* @param array $params
Expand Down Expand Up @@ -315,23 +342,16 @@ protected function _isPublic(array $params) {
$authentication = $this->_getAuth();

foreach ($authentication as $rule) {
if (!empty($params['plugin'])) {
if ($params['plugin'] !== $rule['plugin']) {
continue;
}
} else {
if (!empty($rule['plugin'])) {
continue;
}
// Match plugin and prefix slots using `null`-equivalent semantics:
// a routing param that is missing, null, or an empty string is treated as
// "no plugin / no prefix". Previously `!empty()` swallowed the literal
// strings `'0'` and any value PHP coerces to false (rare for routes, but
// the explicit `null` check is more honest about what the param can be).
if (!$this->_matchesRouteSlot($params['plugin'] ?? null, $rule['plugin'] ?? null)) {
continue;
}
if (!empty($params['prefix'])) {
if ($params['prefix'] !== $rule['prefix']) {
continue;
}
} else {
if (!empty($rule['prefix'])) {
continue;
}
if (!$this->_matchesRouteSlot($params['prefix'] ?? null, $rule['prefix'] ?? null)) {
continue;
}
if ($params['controller'] !== $rule['controller']) {
continue;
Expand Down
50 changes: 33 additions & 17 deletions src/Auth/AllowTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,15 @@ protected function _getAllowRule(array $params) {
$allowDefaults = $this->_getAllowDefaultsForCurrentParams($params);

foreach ($rules as $rule) {
if (isset($params['plugin'])) {
if ($params['plugin'] !== $rule['plugin']) {
continue;
}
} else {
if (!empty($rule['plugin'])) {
continue;
}
// Treat null and empty-string the same on both sides (request and rule); see
// AclTrait::_matchesRouteSlot for the rationale. Inlining the check here
// rather than calling the AclTrait method because this trait lives next to
// AclTrait but is not guaranteed to be composed alongside it.
if (!$this->_matchesAllowSlot($params['plugin'] ?? null, $rule['plugin'] ?? null)) {
continue;
}
if (isset($params['prefix'])) {
if ($params['prefix'] !== $rule['prefix']) {
continue;
}
} else {
if (!empty($rule['prefix'])) {
continue;
}
if (!$this->_matchesAllowSlot($params['prefix'] ?? null, $rule['prefix'] ?? null)) {
continue;
}
if ($params['controller'] !== $rule['controller']) {
continue;
Expand All @@ -65,6 +57,30 @@ protected function _getAllowRule(array $params) {
];
}

/**
* Compare a request-side route slot value (plugin/prefix) against an allow-rule value.
*
* Treats null and the empty string as the "no plugin / no prefix" sentinel; anything
* else must match exactly. See AclTrait::_matchesRouteSlot for the broader rationale.
*
* @param mixed $request
* @param mixed $rule
* @return bool
*/
protected function _matchesAllowSlot($request, $rule): bool {
$requestEmpty = $request === null || $request === '';
$ruleEmpty = $rule === null || $rule === '';

if ($requestEmpty && $ruleEmpty) {
return true;
}
if ($requestEmpty xor $ruleEmpty) {
return false;
}

return $request === $rule;
}

/**
* @param array $rule
* @param string $action
Expand Down Expand Up @@ -107,7 +123,7 @@ protected function _getAllowDefaultsForCurrentParams(array $params) {
$result = [];
if ($allowedPrefixes) {
foreach ($allowedPrefixes as $allowedPrefix) {
if ($params['prefix'] === $allowedPrefix || strpos($params['prefix'], $allowedPrefix . '/') === 0) {
if ($params['prefix'] === $allowedPrefix || str_starts_with((string)$params['prefix'], $allowedPrefix . '/')) {
return ['*'];
}
}
Expand Down
Loading
Loading