| name | check-access-control-model |
| description | Analyzes PHP code for access control issues. Detects inline role checks, hardcoded permissions, mixed ACL/RBAC models, missing Voter/Policy pattern, and authorization logic in controllers. |
Access Control Model Check
Analyze PHP code for access control anti-patterns that lead to inconsistent authorization, privilege escalation, and unmaintainable permission logic.
Detection Patterns
1. Inline Role Checks
<?php
declare(strict_types=1);
final class ArticleController
{
public function delete(Request $request, string $id): Response
{
if ($request->user()->role === 'admin') {
$this->articleService->delete($id);
return new Response(null, 204);
}
return new Response('Forbidden', 403);
}
}
if ($user->getRole() === 'editor' || $user->getRole() === 'admin') {
}
final class ArticleVoter extends Voter
{
protected function supports(string $attribute, mixed $subject): bool
{
return $subject instanceof Article
&& in_array($attribute, ['VIEW', 'EDIT', 'DELETE'], true);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
return match ($attribute) {
'DELETE' => $this->canDelete($user, $subject),
'EDIT' => $this->canEdit($user, $subject),
'VIEW' => true,
default => false,
};
}
private function canDelete(UserInterface $user, Article $article): bool
{
return $user->hasRole('ROLE_ADMIN')
|| $article->authorId()->equals($user->id());
}
private function canEdit(UserInterface $user, Article $article): bool
{
return $user->hasRole('ROLE_EDITOR')
|| $article->authorId()->equals($user->id());
}
}
final class ArticleController
{
public function delete(Request $request, string $id): Response
{
$article = $this->articleRepository->findOrFail(new ArticleId($id));
$this->denyAccessUnlessGranted('DELETE', $article);
$this->articleService->delete($article);
return new Response(null, 204);
}
}
2. Hardcoded Permission Strings
<?php
declare(strict_types=1);
if ($user->hasPermission('manage_users')) { }
if ($user->hasPermission('edit_posts')) { }
if ($user->hasPermission('manage_users')) { }
enum Permission: string
{
case ManageUsers = 'manage_users';
case EditPosts = 'edit_posts';
case ViewReports = 'view_reports';
case DeleteOrders = 'delete_orders';
}
if ($user->hasPermission(Permission::ManageUsers)) { }
3. Mixed Authorization Models
<?php
declare(strict_types=1);
final class UserController
{
public function index(): Response
{
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedHttpException();
}
return new Response($this->userService->list());
}
}
final class OrderController
{
public function show(string $id): Response
{
$order = $this->orderRepo->find($id);
if ($order->userId() !== $this->getUser()->id()) {
throw new AccessDeniedHttpException();
}
return new Response($order);
}
}
final class ReportController
{
public function index(): Response
{
return new Response($this->reportService->generate());
}
}
4. Authorization Logic in Controllers
<?php
declare(strict_types=1);
final class ProjectController
{
public function update(Request $request, string $id): Response
{
$project = $this->projectRepo->find($id);
$user = $request->user();
if ($user->role === 'admin') {
} elseif ($user->role === 'manager' && $project->teamId() === $user->teamId()) {
} elseif ($project->ownerId() === $user->id()) {
} else {
return new Response('Forbidden', 403);
}
$this->projectService->update($project, $request->validated());
return new Response($project);
}
}
#[IsGranted('EDIT', subject: 'project')]
final class ProjectController
{
public function update(Request $request, #[MapEntity] Project $project): Response
{
$this->projectService->update($project, $request->validated());
return new Response($project);
}
}
5. Missing Deny-by-Default
<?php
declare(strict_types=1);
Grep Patterns
Grep: "->role\s*===\s*['\"]|->getRole\(\)\s*===\s*['\"]|role\s*==\s*['\"]" --glob "**/*.php"
Grep: "hasPermission\(['\"]|can\(['\"]|isAllowed\(['\"]" --glob "**/*.php"
Grep: "->role|->getRole|isAdmin|isManager|hasRole" --glob "**/*Controller*.php"
Grep: "->role|->getRole|isAdmin|isManager|hasRole" --glob "**/*Action*.php"
Grep: "extends Voter|extends Policy|implements VoterInterface" --glob "**/*.php"
Grep: "#\[IsGranted|@IsGranted|@Security|denyAccessUnlessGranted" --glob "**/*.php"
Grep: "class.*Controller" --glob "**/*Controller*.php"
Grep: "class.*Action" --glob "**/*Action*.php"
Grep: "enum.*Permission|enum.*Role" --glob "**/*.php"
Severity Classification
| Pattern | Severity |
|---|
| Missing deny-by-default | 🔴 Critical |
| No authorization on admin endpoints | 🔴 Critical |
| Inline role checks with string comparison | 🟠 Major |
| Authorization logic in controllers | 🟠 Major |
| Hardcoded permission strings (no enum) | 🟠 Major |
| Mixed RBAC/ACL models | 🟡 Minor |
| Missing Voter/Policy for resource access | 🟡 Minor |
Output Format
### Access Control Issue: [Brief Description]
**Severity:** 🔴/🟠/🟡
**Location:** `file.php:line`
**Type:** [Inline Check|Hardcoded Permission|Mixed Model|Controller Auth|No Auth]
**Issue:**
[Description of the access control anti-pattern]
**Risk:**
- Privilege escalation via inconsistent checks
- Forgotten authorization on new endpoints
- Permission logic impossible to audit
**Code:**
```php
// Problematic pattern
Fix:
## When This Is Acceptable
- **Public API endpoints** -- Endpoints explicitly designed for unauthenticated access (e.g., login, registration, public data)
- **Internal microservice communication** -- Service-to-service calls behind network security where mutual TLS is used
- **CLI commands** -- Console commands that run with system-level privileges by design
- **Health check endpoints** -- /health, /ready, /live endpoints intended for load balancers
### False Positive Indicators
- Route is explicitly marked as public in security configuration
- Controller is behind a firewall rule that already enforces authentication
- Role check is inside a Voter or Policy class (correct location)